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
99 changes: 99 additions & 0 deletions .github/workflows/deploy-k8s-client-preview.yml
Original file line number Diff line number Diff line change
@@ -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.
57 changes: 57 additions & 0 deletions infra/k8s/preview/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Deploy (or update) a per-PR frontend preview into the existing namespace.
# Usage: deploy.sh <pr-number>
# 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 - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: preview-pr-${PR}
labels:
preview-pr: "${PR}"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
ingressClassName: nginx
tls:
- hosts: ["$HOST"]
secretName: preview-pr-${PR}-tls
rules:
- host: "$HOST"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-client-pr-${PR}
port:
number: 8080
EOF

# restart to pull the image with the latest changes from the PR
kubectl rollout restart "deployment/web-client-pr-${PR}" -n "$NS"
kubectl rollout status "deployment/web-client-pr-${PR}" -n "$NS" --timeout=180s

echo "https://${HOST}/team-devsecops/" >&3
59 changes: 59 additions & 0 deletions infra/k8s/preview/manifests.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading