#!/usr/bin/env bash set -Eeuo pipefail NAMESPACE=go-insane-prd REGISTRY=nexus.urpi.be CERT_MANAGER_VERSION=v1.19.1 LOCAL_PATH_VERSION=v0.0.37 LOCK_NAME=forgejo-prd-deploy-lock if [[ $# -lt 4 ]]; then echo "Usage: $0 [helm args...]" >&2 exit 2 fi CHART=$1 RELEASE=$2 IMAGE_NAME=$3 IMAGE_TAG=$4 shift 4 : "${KUBECONFIG_PRD:?Set the Forgejo secret KUBECONFIG_PRD to the PRD kubeconfig contents}" : "${NEXUS_USERNAME:?Set the Forgejo secret NEXUS_USERNAME}" : "${NEXUS_PASSWORD:?Set the Forgejo secret NEXUS_PASSWORD}" KUBECONFIG="${RUNNER_TEMP:-/tmp}/kubeconfig-prd-${GITHUB_RUN_ID:-$$}" umask 077 printf '%s\n' "$KUBECONFIG_PRD" > "$KUBECONFIG" export KUBECONFIG LOCK_ACQUIRED=false cleanup() { if [[ "$LOCK_ACQUIRED" == true ]]; then kubectl -n "$NAMESPACE" delete configmap "$LOCK_NAME" --ignore-not-found --wait=false >/dev/null 2>&1 || true; fi rm -f "$KUBECONFIG" } trap cleanup EXIT secret_value() { local name=$1 key=$2 encoded encoded="$(kubectl -n "$NAMESPACE" get secret "$name" -o go-template="{{index .data \"${key}\"}}" 2>/dev/null || true)" [[ -z "$encoded" || "$encoded" == "" ]] && return 0 printf '%s' "$encoded" | base64 -d 2>/dev/null || true } kubectl get --raw=/readyz >/dev/null # Create the workload namespace and its Nexus pull credential idempotently. kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - for attempt in {1..360}; do if kubectl -n "$NAMESPACE" create configmap "$LOCK_NAME" --from-literal=owner="${GITHUB_REPOSITORY:-local}:${GITHUB_RUN_ID:-$$}" >/dev/null 2>&1; then LOCK_ACQUIRED=true; break; fi sleep 5 done if [[ "$LOCK_ACQUIRED" != true ]]; then echo "Timed out waiting for another go-insane-prd deploy to release its lock." >&2; exit 1; fi kubectl -n "$NAMESPACE" create secret docker-registry nexus-registry \ --docker-server="$REGISTRY" \ --docker-username="$NEXUS_USERNAME" \ --docker-password="$NEXUS_PASSWORD" \ --dry-run=client -o yaml | kubectl apply -f - # Stateful charts use the cluster default StorageClass. Install the Rancher local # path provisioner only when the cluster has no default class at all. DEFAULT_STORAGE_CLASS="$(kubectl get storageclass -o=jsonpath='{range .items[*]}{.metadata.name}{" "}{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}{"\n"}{end}' | awk '$2 == "true" { print $1; exit }')" if [[ -z "$DEFAULT_STORAGE_CLASS" ]]; then if ! kubectl get storageclass local-path >/dev/null 2>&1 || ! kubectl -n local-path-storage get deployment local-path-provisioner >/dev/null 2>&1; then kubectl apply -f "https://raw.githubusercontent.com/rancher/local-path-provisioner/${LOCAL_PATH_VERSION}/deploy/local-path-storage.yaml" kubectl -n local-path-storage rollout status deployment/local-path-provisioner --timeout=5m fi kubectl annotate storageclass local-path storageclass.kubernetes.io/is-default-class=true --overwrite fi # Applications' production charts request nginx ingress and Let's Encrypt TLS. # Install only missing cluster prerequisites; leave existing installations intact. if ! kubectl get ingressclass nginx >/dev/null 2>&1; then helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update helm repo update ingress-nginx helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx --create-namespace \ --set controller.service.type=NodePort \ --set controller.hostPort.enabled=true \ --set controller.kind=DaemonSet \ --set controller.admissionWebhooks.enabled=false \ --wait --timeout 10m fi if ! kubectl get crd clusterissuers.cert-manager.io >/dev/null 2>&1 || ! kubectl -n cert-manager get deployment cert-manager >/dev/null 2>&1; then kubectl apply --validate=false -f "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml" kubectl -n cert-manager rollout status deployment/cert-manager --timeout=5m kubectl -n cert-manager rollout status deployment/cert-manager-cainjector --timeout=5m kubectl -n cert-manager rollout status deployment/cert-manager-webhook --timeout=5m fi if ! kubectl get clusterissuer letsencrypt-prod >/dev/null 2>&1; then kubectl apply -f - <<'ISSUER' apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: admin@urpi.be privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx ISSUER fi kubectl wait --for=condition=Ready clusterissuer/letsencrypt-prod --timeout=2m if [[ ! -d "$CHART" ]]; then echo "Helm chart not found: $CHART" >&2 exit 1 fi if [[ -f "$CHART/Chart.lock" ]]; then helm dependency build "$CHART"; fi HELM_ARGS=( --namespace "$NAMESPACE" --create-namespace ) if [[ "$RELEASE" != go-guess ]]; then HELM_ARGS+=(--set-string "image.repository=${REGISTRY}/docker-hosted/${IMAGE_NAME}" --set-string "image.tag=${IMAGE_TAG}") fi if [[ -f "$CHART/values-prd.yaml" ]]; then HELM_ARGS+=(--values "$CHART/values-prd.yaml") fi case "$RELEASE" in go-loose) if ! kubectl -n "$NAMESPACE" get secret go-loose-runtime >/dev/null 2>&1; then db_password="$(secret_value go-loose-postgresql postgres-password)" if [[ -z "$db_password" ]] && kubectl -n "$NAMESPACE" get pvc -o name | grep -Eq 'go-loose.*postgresql|data-go-loose'; then echo "Existing go-loose database storage has no recoverable password secret; refusing to rotate its credentials." >&2 exit 1 fi [[ -n "$db_password" ]] || db_password="$(openssl rand -hex 24)" session_secret="$(secret_value go-loose-env GO_LOOSE_SESSION_SECRET)" [[ -n "$session_secret" ]] || session_secret="$(openssl rand -hex 32)" kubectl -n "$NAMESPACE" create secret generic go-loose-runtime --from-literal=postgresql-password="$db_password" --from-literal=session-secret="$session_secret" fi db_password="$(secret_value go-loose-runtime postgresql-password)" session_secret="$(secret_value go-loose-runtime session-secret)" database_url="postgres://goloose:${db_password}@go-loose-postgresql:5432/goloose?sslmode=disable" HELM_ARGS+=(--set-string "postgresql.auth.password=${db_password}" --set-string "envSecrets.GO_LOOSE_DATABASE_URL=${database_url}" --set-string "envSecrets.GO_LOOSE_SESSION_SECRET=${session_secret}") ;; go-mad) if ! kubectl -n "$NAMESPACE" get secret go-mad-runtime >/dev/null 2>&1; then db_password="$(secret_value go-mad-postgresql password)" [[ -n "$db_password" ]] || db_password="$(secret_value go-mad-postgresql postgres-password)" if [[ -z "$db_password" ]] && kubectl -n "$NAMESPACE" get pvc -o name | grep -Eq 'go-mad.*postgresql|data-go-mad'; then echo "Existing go-mad database storage has no recoverable password secret; refusing to rotate its credentials." >&2 exit 1 fi [[ -n "$db_password" ]] || db_password="$(openssl rand -hex 24)" kubectl -n "$NAMESPACE" create secret generic go-mad-runtime --from-literal=postgresql-password="$db_password" --from-literal=jwt-secret="$(openssl rand -hex 32)" fi db_password="$(secret_value go-mad-runtime postgresql-password)" jwt_secret="$(secret_value go-mad-runtime jwt-secret)" HELM_ARGS+=(--set-string "postgresql.auth.password=${db_password}" --set-string "go-mad.jwtSecret=${jwt_secret}") ;; go-first) if ! kubectl -n "$NAMESPACE" get secret go-first-runtime >/dev/null 2>&1; then kubectl -n "$NAMESPACE" create secret generic go-first-runtime --from-literal=jwt-secret="$(openssl rand -hex 32)" fi ;; go-places) : "${GO_LOOSE_JWT_SECRET:?Set Forgejo secret GO_LOOSE_JWT_SECRET to the same signing key used by go-loose}" kubectl -n "$NAMESPACE" create secret generic go-loose-jwt --from-literal=secret="$GO_LOOSE_JWT_SECRET" --dry-run=client -o yaml | kubectl apply -f - ;; go-bananas) : "${GO_BANANAS_OIDC_CLIENT_SECRET:?Set Forgejo secret GO_BANANAS_OIDC_CLIENT_SECRET}" session_secret="$(secret_value go-bananas-auth session-secret)" if [[ -z "$session_secret" ]]; then session_secret="$(openssl rand -hex 32)"; fi kubectl -n "$NAMESPACE" create secret generic go-bananas-auth --from-literal=session-secret="$session_secret" --from-literal=oidc-client-secret="$GO_BANANAS_OIDC_CLIENT_SECRET" --dry-run=client -o yaml | kubectl apply -f - ;; esac helm upgrade --install "$RELEASE" "$CHART" "${HELM_ARGS[@]}" "$@" --wait --timeout 15m kubectl -n "$NAMESPACE" get deployments,statefulsets,pods,services,ingresses