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
46 changes: 5 additions & 41 deletions .github/workflows/cluster-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,45 +80,6 @@ jobs:
EOF
kubectl wait --for=condition=ready pod -l app=minio -n s3proxy --timeout=120s

- name: Deploy Redis
run: |
cat <<EOF | kubectl apply -n s3proxy -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
resources:
limits:
memory: 128Mi
cpu: 100m
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
EOF
kubectl wait --for=condition=ready pod -l app=redis -n s3proxy --timeout=120s

- name: Build Helm dependencies
run: |
helm repo add dandydeveloper https://dandydeveloper.github.io/charts
Expand All @@ -136,8 +97,11 @@ jobs:
--set secrets.encryptKey="test-encryption-key-32chars!!" \
--set secrets.awsAccessKeyId=minioadmin \
--set secrets.awsSecretAccessKey=minioadmin \
--set redis-ha.enabled=false \
--set externalRedis.url="redis://redis:6379/0" \
--set redis-ha.auth=true \
--set redis-ha.redisPassword=testredispassword123 \
--set redis-ha.persistentVolume.enabled=false \
--set redis-ha.hardAntiAffinity=false \
--set redis-ha.haproxy.hardAntiAffinity=false \
--set replicaCount=3 \
--set resources.limits.cpu=100m \
--set resources.requests.cpu=50m \
Expand Down
17 changes: 16 additions & 1 deletion .github/workflows/helm-install-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,13 @@ jobs:
EOF
kubectl wait --for=condition=ready pod -l app=minio -n s3proxy --timeout=120s

- name: Deploy simple Redis
- name: Deploy Redis with password
run: |
# Create Redis password secret
kubectl create secret generic redis-secret \
--from-literal=redis-password=testredispassword123 \
-n s3proxy

cat <<EOF | kubectl apply -n s3proxy -f -
apiVersion: apps/v1
kind: Deployment
Expand All @@ -91,6 +96,14 @@ jobs:
containers:
- name: redis
image: redis:7-alpine
command: ["redis-server"]
args: ["--requirepass", "\$(REDIS_PASSWORD)"]
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-secret
key: redis-password
ports:
- containerPort: 6379
resources:
Expand Down Expand Up @@ -139,6 +152,8 @@ jobs:
--set secrets.awsSecretAccessKey=minioadmin \
--set redis-ha.enabled=false \
--set externalRedis.url="redis://redis:6379/0" \
--set externalRedis.existingSecret=redis-secret \
--set externalRedis.passwordKey=redis-password \
--set replicaCount=3 \
--set resources.limits.cpu=100m \
--set resources.requests.cpu=50m \
Expand Down
33 changes: 0 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,39 +254,6 @@ aws s3 --endpoint-url https://s3proxy.example.com cp file.txt s3://bucket/

> **Recommended for internal access:** Enable both `gateway.enabled=true` and `ingress.enabled=true`. This routes traffic through the ingress controller for load balancing across pods, while providing a convenient internal DNS name (`s3-gateway.<namespace>`) without external DNS configuration.

#### Example: External Access with Ingress

```yaml
# values-prod.yaml
gateway:
enabled: true
ingress:
enabled: true
className: nginx
hosts:
- s3proxy.example.com
tls:
- secretName: s3proxy-tls
hosts:
- s3proxy.example.com
```

```bash
helm install s3proxy ./manifests -f values-prod.yaml \
--set secrets.existingSecrets.enabled=true \
--set secrets.existingSecrets.name=s3proxy-secrets
```

#### Example: Using External Redis (ElastiCache, etc.)

```bash
helm install s3proxy ./manifests \
--set redis-ha.enabled=false \
--set externalRedis.url="redis://my-elasticache.xxx.cache.amazonaws.com:6379/0" \
--set secrets.existingSecrets.enabled=true \
--set secrets.existingSecrets.name=s3proxy-secrets
```

### Health Checks

The proxy exposes health endpoints for Kubernetes probes:
Expand Down
12 changes: 5 additions & 7 deletions e2e/docker-compose.cluster.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,7 @@ services:
echo "✓ Ingress Controller installed"

echo "=== Building s3proxy image ==="
# Skip if image already exists (pre-built in CI)
if docker image inspect s3proxy:latest >/dev/null 2>&1; then
echo "Image already exists, skipping build"
else
docker build -t s3proxy:latest /app
fi
docker build -t s3proxy:latest /app

echo "=== Loading image into kind ==="
kind load docker-image s3proxy:latest --name s3proxy-test
Expand Down Expand Up @@ -164,6 +159,7 @@ services:
helm upgrade --install s3proxy /app/manifests \
-n s3proxy --wait --timeout 1800s \
--set image.repository=s3proxy \
--debug \
--set image.pullPolicy=IfNotPresent \
--set s3.host="http://minio:9000" \
--set secrets.encryptKey="$$ENCRYPT_KEY" \
Expand All @@ -177,7 +173,9 @@ services:
--set redis-ha.hardAntiAffinity=false \
--set redis-ha.affinity=null \
--set redis-ha.haproxy.hardAntiAffinity=false \
--set redis-ha.haproxy.affinity=null
--set redis-ha.haproxy.affinity=null \
--set redis-ha.auth=true \
--set redis-ha.redisPassword=testredispassword123

echo "=== Deployment Status ==="
kubectl get all -n s3proxy
Expand Down
27 changes: 26 additions & 1 deletion manifests/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ spec:
{{- if not .Values.secrets.existingSecrets.enabled }}
- secretRef:
name: {{ printf "%s-secrets" .Chart.Name }}
{{- else }}
{{- end }}
{{- /* 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) }}
{{- if $needsEnv }}
env:
{{- /* App secrets from existing secret */ -}}
{{- if .Values.secrets.existingSecrets.enabled }}
- name: S3PROXY_ENCRYPT_KEY
valueFrom:
secretKeyRef:
Expand All @@ -46,6 +51,26 @@ spec:
secretKeyRef:
name: {{ .Values.secrets.existingSecrets.name }}
key: {{ .Values.secrets.existingSecrets.keys.awsSecretAccessKey }}
{{- end }}
{{- /* Redis password from redis-ha secret */ -}}
{{- if and (index .Values "redis-ha" "enabled") (index .Values "redis-ha" "auth") }}
- name: S3PROXY_REDIS_PASSWORD
valueFrom:
secretKeyRef:
{{- if index .Values "redis-ha" "existingSecret" }}
name: {{ index .Values "redis-ha" "existingSecret" }}
{{- else }}
name: {{ .Release.Name }}-redis-ha
{{- end }}
key: {{ index .Values "redis-ha" "authKey" | default "auth" }}
{{- /* Redis password from external Redis secret */ -}}
{{- else if and (not (index .Values "redis-ha" "enabled")) .Values.externalRedis.existingSecret }}
- name: S3PROXY_REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.externalRedis.existingSecret }}
key: {{ .Values.externalRedis.passwordKey | default "redis-password" }}
{{- end }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
Expand Down
16 changes: 13 additions & 3 deletions manifests/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,26 @@ performance:

# External Redis (for managed services)
externalRedis:
url: ""
url: "" # e.g., "redis://host:6379/0" or "redis://:password@host:6379/0"
uploadTtlHours: 24
# Password can be provided in one of two ways:
# 1. Embedded in URL above: redis://:password@host:6379/0
# 2. Via existingSecret (recommended - keeps password out of configmap)
existingSecret: ""
passwordKey: "redis-password"

# Redis HA (embedded)
redis-ha:
enabled: true
replicas: 3
# Redis authentication - when auth: true, you MUST provide one of:
# existingSecret: name of a pre-existing secret (recommended for production)
# redisPassword: password value (chart will create a secret)
# The secret key name is configured via authKey (default: "auth")
auth: false
redisPassword: ""
existingSecret: ""
authKey: "auth"

persistentVolume:
enabled: true
Expand Down Expand Up @@ -62,8 +74,6 @@ redis-ha:
min-replicas-to-write: 1
min-replicas-max-lag: 5

auth: false
authKey: ""
hardAntiAffinity: true

resources:
Expand Down
1 change: 1 addition & 0 deletions s3proxy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Settings(BaseSettings):

# Redis settings (for distributed state in HA deployments)
redis_url: str = Field(default="", description="Redis URL for HA mode (empty = in-memory single-instance)")
redis_password: str = Field(default="", description="Redis password (optional, can also be in URL)")
redis_upload_ttl_hours: int = Field(default=24, description="TTL for upload state in Redis (hours)")

# Logging
Expand Down
2 changes: 1 addition & 1 deletion s3proxy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def create_lifespan(settings: Settings) -> "AsyncIterator[None]":
async def lifespan(_app: FastAPI) -> "AsyncIterator[None]":
logger.info("Starting", endpoint=settings.s3_endpoint, port=settings.port)
# Initialize Redis if configured (for HA), otherwise use in-memory storage
await init_redis(settings.redis_url or None)
await init_redis(settings.redis_url or None, settings.redis_password or None)
yield
# Close Redis connection if active
await close_redis()
Expand Down
9 changes: 7 additions & 2 deletions s3proxy/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@ def json_loads(data: bytes) -> dict:
_use_redis: bool = False


async def init_redis(redis_url: str | None) -> "Redis | None":
async def init_redis(redis_url: str | None, redis_password: str | None = None) -> "Redis | None":
"""Initialize Redis connection pool if URL is provided.

Args:
redis_url: Redis URL or None/empty to use in-memory storage
redis_password: Optional password (overrides any password in URL)

Returns:
Redis client if connected, None if using in-memory storage
Expand All @@ -72,7 +73,11 @@ async def init_redis(redis_url: str | None) -> "Redis | None":
_use_redis = False
return None

_redis_client = redis.from_url(redis_url, decode_responses=False)
# Pass password separately if provided (overrides URL password)
if redis_password:
_redis_client = redis.from_url(redis_url, password=redis_password, decode_responses=False)
else:
_redis_client = redis.from_url(redis_url, decode_responses=False)
# Test connection
await _redis_client.ping()
_use_redis = True
Expand Down