diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fca0e8f..17f2c3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,28 +5,60 @@ on: branches: [ "dev" ] pull_request: branches: [ "dev" ] + workflow_dispatch: + inputs: + frontend_image: + description: 'Frontend image to use in E2E tests (optional)' + required: false + default: 'ghcr.io/morbalint/kemkas-frontend:latest' + e2e_image: + description: 'E2E image to use in E2E tests (optional)' + required: false + default: 'ghcr.io/morbalint/kemkas-frontend:latest' jobs: - docker-be-build: + test-and-build: runs-on: ubuntu-latest permissions: + contents: read packages: write + attestations: write + id-token: write + outputs: + image_sha_tag: ${{ steps.image-tag.outputs.image_sha_tag }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-dotnet@v5 with: dotnet-version: 10.0.x - name: Run unit tests run: dotnet test Kemkas.slnx -c Release - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - run: | - echo "GITHUB_SHA_SHORT=$(echo ${GITHUB_SHA::8})" >> $GITHUB_ENV - echo "registry.digitalocean.com/kemkas/kemkas-be:${GITHUB_SHA::8}" - echo "ghcr.io/${{ github.repository_owner }}/kemkas-backend:${GITHUB_SHA::8}" - - uses: docker/build-push-action@v5 + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Compute image tag + id: image-tag + run: | + image_repo="ghcr.io/${{ github.repository_owner }}/kemkas-backend" + sha_tag="${image_repo}:${GITHUB_SHA::8}" + + tags="$sha_tag" + if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then + tags+=$'\n'"${image_repo}:latest" + fi + + echo "image_sha_tag=${sha_tag}" >> "$GITHUB_OUTPUT" + { + echo "tags<> "$GITHUB_OUTPUT" + - uses: docker/build-push-action@v7 with: context: . file: Kemkas.Web/Dockerfile @@ -35,21 +67,50 @@ jobs: push: true load: true tags: | - ghcr.io/${{ github.repository_owner }}/kemkas-backend:${{ env.GITHUB_SHA_SHORT }} + ${{ steps.image-tag.outputs.tags }} -# - uses: actions/checkout@v4 -# with: -# repository: ${{ github.repository_owner }}/kemkas-deployment -# ref: refs/heads/main -# path: ./deployment-repo -# clean: false -# ssh-key: ${{ secrets.DEPLOYMENT_REPO_DEPLOY_KEY }} -# - uses: mikefarah/yq@v4 -# with: -# cmd: yq -i '.spec.template.spec.containers[0].image = "registry.digitalocean.com/kemkas/kemkas-be:${{ env.GITHUB_SHA_SHORT }}"' ./deployment-repo/kemkas-beta/kemkas-be-deployment.yaml -# - run: | -# cd ./deployment-repo -# git config --add user.email "deployment-bot@kemkas.hu" -# git config --add user.name "Deployment Bot" -# git commit -a -m "deploy registry.digitalocean.com/kemkas/kemkas-be:${{ env.GITHUB_SHA_SHORT }}" -# git push + e2e: + runs-on: ubuntu-latest + needs: + - test-and-build + permissions: + contents: read + packages: read + env: + BACKEND_IMAGE: ${{ needs.test-and-build.outputs.image_sha_tag }} + FRONTEND_IMAGE: ${{ github.event.inputs.frontend_image || 'ghcr.io/morbalint/kemkas-frontend:latest' }} + E2E_IMAGE: ${{ github.event.inputs.e2e_image || 'ghcr.io/morbalint/kemkas-e2e:latest' }} + steps: + - uses: actions/checkout@v6 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Setup docker compose + uses: docker/setup-compose-action@v2 + - name: Pull images for test run + run: docker compose -f e2e.docker-compose.yaml pull + - name: Run Docker Compose E2E tests + run: docker compose -f e2e.docker-compose.yaml up --no-build --abort-on-container-exit --exit-code-from e2e e2e + - name: Print logs + if: failure() + run: docker compose -f e2e.docker-compose.yaml logs + - name: Upload Playwright HTML report + if: always() + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: e2e/playwright-report + if-no-files-found: ignore + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@v7 + with: + name: playwright-test-results + path: e2e/test-results + if-no-files-found: ignore + - name: Tear down Docker Compose stack + if: always() + run: docker compose -f e2e.docker-compose.yaml down -v --remove-orphans diff --git a/.gitignore b/.gitignore index 8ad02fa..21530be 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ yarn-error.log* /tmp /temp /local +/e2e/ obj/ bin/ diff --git a/Kemkas.slnx b/Kemkas.slnx index db56216..2b60acb 100644 --- a/Kemkas.slnx +++ b/Kemkas.slnx @@ -3,6 +3,7 @@ + diff --git a/e2e.docker-compose.yaml b/e2e.docker-compose.yaml new file mode 100644 index 0000000..60b4b74 --- /dev/null +++ b/e2e.docker-compose.yaml @@ -0,0 +1,79 @@ +services: + certs: + image: alpine:3.21 + volumes: + - tls-certs:/certs + - ./scripts/generate-e2e-certs.sh:/scripts/generate-e2e-certs.sh:ro + command: ["sh", "/scripts/generate-e2e-certs.sh"] + + db: + image: postgres:16.13-alpine3.23 + environment: + POSTGRES_USER: "kemkas" + POSTGRES_PASSWORD: "kemkas-dev42" + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U kemkas -d postgres"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 5s + + backend: + image: ${BACKEND_IMAGE:-ghcr.io/morbalint/kemkas-backend:b8565f8e} + build: + context: . + dockerfile: Kemkas.Web/Dockerfile + environment: + ConnectionStrings__DefaultConnection: "Server=db;User Id=kemkas;Password=kemkas-dev42" + Email__DomainName: "mail.kemkas.hu" + Email__ApiKey: "" + depends_on: + db: + condition: service_healthy + + frontend: + image: ${FRONTEND_IMAGE:-ghcr.io/morbalint/kemkas-frontend:latest} + + proxy: + image: nginx:1.29.7-alpine3.23 + ports: + - "8000:8000" + volumes: + - ./e2e.nginx.conf:/etc/nginx/nginx.conf:ro + - tls-certs:/etc/nginx/certs:ro + depends_on: + certs: + condition: service_completed_successfully + backend: + condition: service_started + frontend: + condition: service_started + healthcheck: + test: ["CMD-SHELL", "wget -q --spider --no-check-certificate https://127.0.0.1:8000 || exit 1"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 5s + + e2e: + image: ${E2E_IMAGE:-ghcr.io/morbalint/kemkas-e2e:latest} + environment: + E2E_BASE_URL: https://proxy:8000 + E2E_USE_WEBSERVER: "false" + E2E_TRUSTED_CERT_PATH: /certs/rootCA.pem + E2E_WAIT_FOR_URL: https://proxy:8000 + volumes: + - tls-certs:/certs:ro + - ./e2e/playwright-report:/work/playwright-report + - ./e2e/test-results:/work/test-results + depends_on: + certs: + condition: service_completed_successfully + proxy: + condition: service_healthy + +volumes: + db-data: + tls-certs: diff --git a/e2e.nginx.conf b/e2e.nginx.conf new file mode 100644 index 0000000..8a69c61 --- /dev/null +++ b/e2e.nginx.conf @@ -0,0 +1,96 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + keepalive_timeout 65; + + upstream frontend { + server frontend:80; + } + + upstream backend { + server backend:8080; + } + + server { + listen 8000 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/certs/kemkas.pem; + ssl_certificate_key /etc/nginx/certs/kemkas.key; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + proxy_pass http://frontend; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + + location /Identity/ { + proxy_pass_request_headers on; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "$http_host"; + proxy_pass http://backend; + } + + location /api/ { + proxy_pass_request_headers on; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "$http_host"; + proxy_pass http://backend; + } + + location /signin- { + proxy_pass_request_headers on; + proxy_set_header X-Forwarded-For "$proxy_add_x_forwarded_for"; + proxy_set_header X-Forwarded-Proto "$scheme"; + proxy_set_header X-Forwarded-Host "$http_host"; + proxy_pass http://backend; + } + + location /lib/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + + location /js/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + + location /css/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + + location /img/ { + proxy_pass_request_headers on; + proxy_pass http://backend; + } + } +} diff --git a/scripts/generate-e2e-certs.sh b/scripts/generate-e2e-certs.sh new file mode 100644 index 0000000..ae147b8 --- /dev/null +++ b/scripts/generate-e2e-certs.sh @@ -0,0 +1,61 @@ +#!/bin/sh +set -eu + +CERT_DIR="${CERT_DIR:-/certs}" + +mkdir -p "$CERT_DIR" + +if [ -f "$CERT_DIR/rootCA.pem" ] && [ -f "$CERT_DIR/rootCA.key" ] && [ -f "$CERT_DIR/kemkas.pem" ] && [ -f "$CERT_DIR/kemkas.key" ]; then + echo "Using existing TLS material from $CERT_DIR" + exit 0 +fi + +apk add --no-cache openssl >/dev/null + +cat > /tmp/kemkas-server.cnf <<'EOF' +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext + +[dn] +CN = localhost + +[req_ext] +subjectAltName = @alt_names +keyUsage = digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth + +[alt_names] +DNS.1 = localhost +DNS.2 = proxy +DNS.3 = host.docker.internal +IP.1 = 127.0.0.1 +IP.2 = ::1 +EOF + +cat > /tmp/kemkas-v3.ext <<'EOF' +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage=digitalSignature,keyEncipherment +extendedKeyUsage=serverAuth +subjectAltName=@alt_names + +[alt_names] +DNS.1=localhost +DNS.2=proxy +DNS.3=host.docker.internal +IP.1=127.0.0.1 +IP.2=::1 +EOF + +openssl genrsa -out "$CERT_DIR/rootCA.key" 2048 +openssl req -x509 -new -nodes -key "$CERT_DIR/rootCA.key" -sha256 -days 3650 -subj "/CN=kemkas-local-root" -out "$CERT_DIR/rootCA.pem" + +openssl genrsa -out "$CERT_DIR/kemkas.key" 2048 +openssl req -new -key "$CERT_DIR/kemkas.key" -out /tmp/kemkas.csr -config /tmp/kemkas-server.cnf +openssl x509 -req -in /tmp/kemkas.csr -CA "$CERT_DIR/rootCA.pem" -CAkey "$CERT_DIR/rootCA.key" -CAcreateserial -out "$CERT_DIR/kemkas.pem" -days 825 -sha256 -extfile /tmp/kemkas-v3.ext + +echo "Generated TLS CA and nginx certificate under $CERT_DIR"