diff --git a/.github/workflows/deploy-k8s-client-preview.yml b/.github/workflows/deploy-k8s-client-preview.yml new file mode 100644 index 0000000..22baba0 --- /dev/null +++ b/.github/workflows/deploy-k8s-client-preview.yml @@ -0,0 +1,99 @@ +name: PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + paths: + - 'web-client/**' + - 'infra/k8s/preview/**' + - '.github/workflows/deploy-k8s-client-preview.yml' + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + pull-requests: write + +env: + # gets prefixed with the PR number + PREVIEW_DOMAIN: devsecops.stud.k8s.aet.cit.tum.de + +jobs: + deploy: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + environment: kubernetes + steps: + - name: Checkout PR head + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Image prefix + run: echo "IMAGE_PREFIX=ghcr.io/$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build web-client + uses: docker/build-push-action@v6 + with: + context: web-client + push: true + tags: ${{ env.IMAGE_PREFIX }}/web-client:preview-pr-${{ github.event.pull_request.number }} + + - name: Set up kubectl + uses: azure/setup-kubectl@v4 + + - name: Configure kubeconfig + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Deploy preview + id: deploy + env: + WEB_CLIENT_IMAGE: ${{ env.IMAGE_PREFIX }}/web-client:preview-pr-${{ github.event.pull_request.number }} + run: | + url=$(infra/k8s/preview/deploy.sh "${{ github.event.pull_request.number }}") + echo "url=$url" >> "$GITHUB_OUTPUT" + + - name: Comment preview URL + uses: marocchino/sticky-pull-request-comment@v2 + with: + number: ${{ github.event.pull_request.number }} + header: preview + message: ๐Ÿš€ [Frontend preview โ†—](${{ steps.deploy.outputs.url }})  ยท  commit `${{ github.event.pull_request.head.sha }}` + + teardown: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + environment: kubernetes + steps: + - uses: actions/checkout@v4 + - name: Set up kubectl + uses: azure/setup-kubectl@v4 + - name: Configure kubeconfig + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + - name: Tear down preview + run: | + PR="${{ github.event.pull_request.number }}" + kubectl delete deployment,service,ingress -n app -l "preview-pr=$PR" + kubectl delete secret "preview-pr-$PR-tls" -n app --ignore-not-found + - name: Update comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + number: ${{ github.event.pull_request.number }} + header: preview + message: ๐Ÿงน The preview environment was removed because this PR was closed. diff --git a/infra/k8s/preview/deploy.sh b/infra/k8s/preview/deploy.sh new file mode 100755 index 0000000..8cbafc1 --- /dev/null +++ b/infra/k8s/preview/deploy.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Deploy (or update) a per-PR frontend preview into the existing namespace. +# Usage: deploy.sh +# Env: WEB_CLIENT_IMAGE (required) โ€” web-client image ref to deploy +# PREVIEW_DOMAIN (default devsecops.stud.k8s.aet.cit.tum.de) +# PREVIEW_NAMESPACE (default app) +set -euo pipefail + +PR="${1:?pr number required}" +export PR +NS="${PREVIEW_NAMESPACE:-app}" +HOST="pr-${PR}.${PREVIEW_DOMAIN:-devsecops.stud.k8s.aet.cit.tum.de}" +export WEB_CLIENT_IMAGE="${WEB_CLIENT_IMAGE:?}" +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Route command output to stderr so stdout carries only the final URL line +exec 3>&1 1>&2 + +# web-client Deployment + Service, named/labelled per PR +envsubst '${PR} ${WEB_CLIENT_IMAGE}' < "$DIR/manifests.yaml" | kubectl apply -n "$NS" -f - + +# setup TLS and expose the deployment using an Ingress +# (this is done inline to easily substitute $HOST) +kubectl apply -n "$NS" -f - <&3 diff --git a/infra/k8s/preview/manifests.yaml b/infra/k8s/preview/manifests.yaml new file mode 100644 index 0000000..ba236bf --- /dev/null +++ b/infra/k8s/preview/manifests.yaml @@ -0,0 +1,59 @@ +--- +# Per-PR frontend preview +# ${PR} and ${WEB_CLIENT_IMAGE} need to be substituted by the caller +apiVersion: apps/v1 +kind: Deployment +metadata: + name: web-client-pr-${PR} + labels: + preview-pr: "${PR}" +spec: + replicas: 1 + selector: + matchLabels: + app: web-client-pr-${PR} + template: + metadata: + labels: + app: web-client-pr-${PR} + preview-pr: "${PR}" + spec: + containers: + - name: web-client + image: ${WEB_CLIENT_IMAGE} + imagePullPolicy: Always + ports: + - containerPort: 8080 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + livenessProbe: + httpGet: + path: /team-devsecops/ + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /team-devsecops/ + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +# expose the pod at web-client:8080 +apiVersion: v1 +kind: Service +metadata: + name: web-client-pr-${PR} + labels: + preview-pr: "${PR}" +spec: + selector: + app: web-client-pr-${PR} + ports: + - port: 8080 + targetPort: 8080