Helm Install Test #87
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Helm Install Test | |
| on: | |
| schedule: | |
| # Run daily at 6 AM UTC | |
| - cron: '0 6 * * *' | |
| workflow_dispatch: | |
| # Explicit permissions are required for GITHUB_TOKEN to pull from GHCR | |
| permissions: | |
| contents: read | |
| packages: read | |
| jobs: | |
| helm-install: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Set up Helm | |
| uses: azure/setup-helm@v5.0.0 | |
| - name: Log in to Container Registry | |
| run: | | |
| echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Create Kind cluster | |
| uses: helm/kind-action@v1 | |
| with: | |
| node_image: kindest/node:v1.29.2 | |
| cluster_name: helm-test | |
| - name: Deploy MinIO as S3 backend | |
| run: | | |
| kubectl create namespace s3proxy | |
| cat <<EOF | kubectl apply -n s3proxy -f - | |
| apiVersion: apps/v1 | |
| kind: Deployment | |
| metadata: | |
| name: minio | |
| spec: | |
| replicas: 1 | |
| selector: | |
| matchLabels: | |
| app: minio | |
| template: | |
| metadata: | |
| labels: | |
| app: minio | |
| spec: | |
| containers: | |
| - name: minio | |
| image: minio/minio:latest | |
| args: ["server", "/data"] | |
| env: | |
| - name: MINIO_ROOT_USER | |
| value: minioadmin | |
| - name: MINIO_ROOT_PASSWORD | |
| value: minioadmin | |
| ports: | |
| - containerPort: 9000 | |
| --- | |
| apiVersion: v1 | |
| kind: Service | |
| metadata: | |
| name: minio | |
| spec: | |
| selector: | |
| app: minio | |
| ports: | |
| - port: 9000 | |
| EOF | |
| kubectl wait --for=condition=ready pod -l app=minio -n s3proxy --timeout=120s | |
| - name: Create K8s Image Pull Secret & Patch Namespace | |
| run: | | |
| # 1. Create the secret using the workflow token | |
| kubectl create secret docker-registry ghcr-login \ | |
| --docker-server=ghcr.io \ | |
| --docker-username=${{ github.actor }} \ | |
| --docker-password=${{ secrets.GITHUB_TOKEN }} \ | |
| --namespace s3proxy \ | |
| --dry-run=client -o yaml | kubectl apply -f - | |
| # 2. Patch the default service account to automatically use this secret | |
| # This acts as a fail-safe if the Helm 'imagePullSecrets' set doesn't propagate | |
| kubectl patch serviceaccount default -n s3proxy -p '{"imagePullSecrets": [{"name": "ghcr-login"}]}' | |
| - name: Install chart from GHCR | |
| run: | | |
| OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | |
| helm install s3proxy oci://ghcr.io/${OWNER}/charts/s3proxy-python --version 0.0.0-latest \ | |
| --namespace s3proxy \ | |
| --set image.repository=ghcr.io/${OWNER}/s3proxy-python \ | |
| --set image.tag=latest \ | |
| --set image.pullPolicy=Always \ | |
| --set "imagePullSecrets[0].name=ghcr-login" \ | |
| --set s3.host="http://minio:9000" \ | |
| --set secrets.credentials[0].accessKey=minioadmin \ | |
| --set secrets.credentials[0].secretKey=minioadmin \ | |
| --set secrets.credentials[0].kek=test-encryption-key-for-ci \ | |
| --set redis.enabled=true \ | |
| --set redis.auth.enabled=true \ | |
| --set redis.auth.password=testredispassword123 \ | |
| --set dashboard.enabled=true \ | |
| --set dashboard.secret=test-dashboard-secret-for-ci \ | |
| --set dashboard.frontend.image.repository=ghcr.io/${OWNER}/s3proxy-dashboard \ | |
| --set dashboard.frontend.image.tag=latest \ | |
| --set dashboard.frontend.image.pullPolicy=Always \ | |
| --set frontproxy.enabled=true \ | |
| --set replicaCount=3 \ | |
| --set resources.limits.cpu=100m \ | |
| --set resources.requests.cpu=50m \ | |
| --wait \ | |
| --timeout 5m | |
| - name: Verify pods are running | |
| run: | | |
| kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=s3proxy-python -n s3proxy --timeout=120s | |
| kubectl get pods -n s3proxy | |
| POD_COUNT=$(kubectl get pods -n s3proxy -l app.kubernetes.io/name=s3proxy-python --no-headers | grep Running | wc -l) | |
| if [ "$POD_COUNT" -lt 3 ]; then | |
| echo "Expected 3 s3proxy pods, got $POD_COUNT" | |
| exit 1 | |
| fi | |
| echo "✓ All 3 s3proxy pods running" | |
| - name: Verify front proxy | |
| run: | | |
| set -euo pipefail | |
| echo "=== Front proxy pods ready ===" | |
| kubectl wait --for=condition=ready pod \ | |
| -l app.kubernetes.io/component=frontproxy -n s3proxy --timeout=120s | |
| kubectl get pods -n s3proxy -l app.kubernetes.io/component=frontproxy | |
| echo "=== S3 round-trip THROUGH the front proxy ===" | |
| kubectl run frontproxy-test -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 | |
| ENDPOINT="http://s3proxy-python-frontproxy:80" | |
| # HAProxy starts with no backend addresses (server-template + init-addr none) | |
| # and only fills its server slots once it resolves the headless Service via DNS, | |
| # which publishes pod IPs only after the app pods are Ready. Until that first | |
| # resolution lands, every request is 503 <NOSRV>. Wait for a live backend before | |
| # asserting the round-trip. | |
| echo "Waiting for the front proxy to discover a backend..." | |
| for i in $(seq 1 30); do | |
| if aws --endpoint-url $ENDPOINT s3 ls >/dev/null 2>&1; then | |
| echo "Backend is live (attempt $i)" | |
| break | |
| fi | |
| [ "$i" = "30" ] && { echo "✗ front proxy never reached a live backend"; exit 1; } | |
| sleep 2 | |
| done | |
| echo "via front proxy - $(date)" > /tmp/fp.txt | |
| ORIG=$(md5sum /tmp/fp.txt | cut -c1-32) | |
| aws --endpoint-url $ENDPOINT s3 mb s3://frontproxy-bucket | |
| aws --endpoint-url $ENDPOINT s3 cp /tmp/fp.txt s3://frontproxy-bucket/fp.txt | |
| aws --endpoint-url $ENDPOINT s3 cp s3://frontproxy-bucket/fp.txt /tmp/down.txt | |
| DOWN=$(md5sum /tmp/down.txt | cut -c1-32) | |
| [ "$ORIG" = "$DOWN" ] || { echo "✗ round-trip via front proxy failed"; exit 1; } | |
| aws --endpoint-url $ENDPOINT s3 rm s3://frontproxy-bucket/fp.txt | |
| aws --endpoint-url $ENDPOINT s3 rb s3://frontproxy-bucket | |
| echo "✓ S3 round-trip through front proxy succeeded" | |
| ' | |
| - name: Check health endpoint | |
| run: | | |
| kubectl port-forward svc/s3proxy-python 4433:4433 -n s3proxy & | |
| sleep 5 | |
| curl -sf http://localhost:4433/healthz && echo "Health check passed" | |
| - name: Run S3 smoke test | |
| run: | | |
| kubectl run s3-smoke-test -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 | |
| ENDPOINT="http://s3proxy-python:4433" | |
| echo "=== Creating test bucket ===" | |
| aws --endpoint-url $ENDPOINT s3 mb s3://smoke-test-bucket | |
| echo "=== Uploading test file ===" | |
| echo "Hello from CI smoke test - $(date)" > /tmp/test.txt | |
| ORIG_MD5=$(md5sum /tmp/test.txt | cut -c1-32) | |
| aws --endpoint-url $ENDPOINT s3 cp /tmp/test.txt s3://smoke-test-bucket/test.txt | |
| echo "=== Listing bucket ===" | |
| aws --endpoint-url $ENDPOINT s3 ls s3://smoke-test-bucket/ | |
| echo "=== Downloading and verifying ===" | |
| aws --endpoint-url $ENDPOINT s3 cp s3://smoke-test-bucket/test.txt /tmp/downloaded.txt | |
| DOWN_MD5=$(md5sum /tmp/downloaded.txt | cut -c1-32) | |
| if [ "$ORIG_MD5" = "$DOWN_MD5" ]; then | |
| echo "✓ Round-trip successful - checksums match" | |
| else | |
| echo "✗ Checksum mismatch!" | |
| exit 1 | |
| fi | |
| echo "=== Verifying encryption (raw read from MinIO) ===" | |
| aws --endpoint-url http://minio:9000 s3 cp s3://smoke-test-bucket/test.txt /tmp/raw.txt 2>/dev/null || true | |
| if [ -f /tmp/raw.txt ]; then | |
| RAW_MD5=$(md5sum /tmp/raw.txt | cut -c1-32) | |
| if [ "$ORIG_MD5" != "$RAW_MD5" ]; then | |
| echo "✓ Data is encrypted - raw content differs from original" | |
| else | |
| echo "✗ Data NOT encrypted - raw matches original!" | |
| exit 1 | |
| fi | |
| fi | |
| echo "=== Cleanup ===" | |
| aws --endpoint-url $ENDPOINT s3 rm s3://smoke-test-bucket/test.txt | |
| aws --endpoint-url $ENDPOINT s3 rb s3://smoke-test-bucket | |
| echo "" | |
| echo "✓ All smoke tests passed!" | |
| ' | |
| - name: Verify dashboard | |
| run: | | |
| set -euo pipefail | |
| ENDPOINT="http://s3proxy-python:4433" | |
| BUCKET="dashboard-ci" | |
| KEY="dashboard-ci.txt" | |
| echo "=== Dashboard pod is ready ===" | |
| kubectl wait --for=condition=ready pod \ | |
| -l app.kubernetes.io/component=dashboard -n s3proxy --timeout=120s | |
| kubectl get pods -n s3proxy -l app.kubernetes.io/component=dashboard | |
| echo "=== Upload an object through the proxy (so the dashboard has traffic) ===" | |
| kubectl run dashboard-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 '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)\" = 'dashboard ci object' | |
| echo UPLOAD_OK | |
| " | |
| # The dashboard is single-origin behind its own service: nginx serves the | |
| # static SPA and reverse-proxies /dashboard/api to the proxy. Port-forward | |
| # the dashboard service and exercise the whole thing through it. | |
| echo "=== Port-forward the dashboard service ===" | |
| kubectl port-forward svc/s3proxy-python-dashboard 18080:80 -n s3proxy & | |
| PF_PID=$! | |
| trap "kill $PF_PID 2>/dev/null || true" EXIT | |
| BASE="http://localhost:18080/dashboard" | |
| for i in $(seq 1 15); do curl -sf -o /dev/null "$BASE" && break; sleep 1; done | |
| echo "=== 1. Static SPA shell is served ===" | |
| curl -sf "$BASE" | grep -q "_app/" || { echo "✗ SPA shell not served at $BASE"; exit 1; } | |
| curl -sf -o /dev/null "$BASE/login" # SPA fallback for the login route | |
| echo "✓ SPA shell served" | |
| echo "=== 2. Auth is enforced (API reverse-proxied through the dashboard) ===" | |
| CODE=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/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 "$BASE/api/status") | |
| [ "$CODE" = "401" ] || { echo "✗ AWS creds accepted as dashboard login ($CODE) — fallback not removed"; exit 1; } | |
| echo "✓ Unauthenticated + AWS-cred requests rejected (401)" | |
| echo "=== 3. Status API works with dashboard credentials (admin/admin) ===" | |
| STATUS=$(curl -sf -u admin:admin "$BASE/api/status") | |
| echo "$STATUS" | grep -q '"status":"Running"' || { echo "✗ status not Running: $STATUS"; exit 1; } | |
| echo "✓ Status API healthy" | |
| echo "=== 4. Bucket listing API shows the uploaded object ===" | |
| BUCKETS=$(curl -sf -u admin:admin "$BASE/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 "$BASE/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 "✓ Dashboard verified end-to-end" | |
| - name: Show logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== Pod Status ===" | |
| kubectl get pods -n s3proxy -o wide | |
| echo "" | |
| echo "=== Describe Failed Pods ===" | |
| kubectl describe pods -n s3proxy -l app.kubernetes.io/name=s3proxy-python | |
| echo "" | |
| echo "=== S3Proxy Logs ===" | |
| kubectl logs -l app.kubernetes.io/name=s3proxy-python -n s3proxy --tail=100 | |
| echo "" | |
| echo "=== Front Proxy Logs ===" | |
| kubectl logs -l app.kubernetes.io/component=frontproxy -n s3proxy --tail=100 | |
| echo "" | |
| echo "=== MinIO Logs ===" | |
| kubectl logs -l app=minio -n s3proxy --tail=50 | |
| echo "" | |
| echo "=== Events ===" | |
| kubectl get events -n s3proxy --sort-by=.lastTimestamp |