provisioning-catalog/components/woodpecker/cluster/woodpecker-lib.sh

382 lines
11 KiB
Bash
Raw Normal View History

# Methods library for woodpecker — sourced by install-woodpecker.sh (and the
# orchestrator's generated run-*.sh). SCRIPT_DIR, CATALOG_LIB and WOODPECKER_* env
# must be set before sourcing. Mirrors the forgejo-lib.sh structure.
_kubectl() {
if command -v kubectl >/dev/null 2>&1; then
command kubectl "$@"
elif command -v k0s >/dev/null 2>&1; then
k0s kubectl "$@"
else
echo "ERROR: no kubectl or k0s binary found" >&2; exit 127
fi
}
_require_env() {
local var="$1"
if [ -z "${!var:-}" ]; then
echo "ERROR: required variable $var is not set" >&2
exit 1
fi
}
_namespace_exists() {
_kubectl get namespace "$WOODPECKER_NAMESPACE" >/dev/null 2>&1
}
_ensure_database() {
local pg_pod
pg_pod=$(_kubectl get pod --namespace "$WOODPECKER_DB_NAMESPACE" \
--selector "app.kubernetes.io/name=postgresql" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [ -z "$pg_pod" ]; then
echo "ERROR: no postgresql pod found in namespace $WOODPECKER_DB_NAMESPACE" >&2
exit 1
fi
local exists
exists=$(_kubectl exec --namespace "$WOODPECKER_DB_NAMESPACE" "$pg_pod" -- \
psql -U postgres -tAc \
"SELECT 1 FROM pg_database WHERE datname = '${WOODPECKER_DATABASE}'" 2>/dev/null || true)
if [ "${exists}" != "1" ]; then
_kubectl exec --namespace "$WOODPECKER_DB_NAMESPACE" "$pg_pod" -- \
psql -U postgres -c \
"CREATE DATABASE \"${WOODPECKER_DATABASE}\"
WITH OWNER postgres
ENCODING 'UTF8'
LC_COLLATE 'C'
LC_CTYPE 'C'
TEMPLATE template0"
echo " [ok] database '${WOODPECKER_DATABASE}' created"
else
echo " [skip] database '${WOODPECKER_DATABASE}' already exists"
fi
_kubectl exec --namespace "$WOODPECKER_DB_NAMESPACE" "$pg_pod" -- \
psql -U postgres -d "${WOODPECKER_DATABASE}" -c \
"GRANT ALL ON SCHEMA public TO postgres"
echo " [ok] schema public grants ensured"
}
_wait_for_deployment() {
local name="$1"
local timeout="${2:-180}"
_kubectl rollout status deployment/"$name" \
--namespace "$WOODPECKER_NAMESPACE" \
--timeout="${timeout}s"
}
# shellcheck source=../../../lib/gateway-tls.sh
source "${CATALOG_LIB}/gateway-tls.sh"
_do_install() {
[ -f "${SCRIPT_DIR}/_credentials.env" ] && set -a && source "${SCRIPT_DIR}/_credentials.env" && set +a
_require_env WOODPECKER_AGENT_SECRET
_require_env WOODPECKER_FORGEJO_SECRET
_require_env WOODPECKER_FORGEJO_CLIENT
_require_env WOODPECKER_HOSTNAME
_require_env POSTGRES_PASSWORD
echo "Ensuring database '${WOODPECKER_DATABASE}' exists..."
_ensure_database
if ! _namespace_exists; then
_kubectl create namespace "$WOODPECKER_NAMESPACE"
fi
local _forgejo_host _forgejo_gw_ip _host_aliases_yaml=""
_forgejo_host=$(echo "${WOODPECKER_FORGEJO_URL}" | sed 's|https\?://||' | cut -d: -f1 | cut -d/ -f1)
if [ -n "${_forgejo_host:-}" ]; then
_forgejo_gw_ip="${WOODPECKER_FORGEJO_GATEWAY_IP:-$(\
_kubectl get svc -n "${WOODPECKER_GATEWAY_NS}" \
-l "gateway.networking.k8s.io/gateway-name=${WOODPECKER_GATEWAY_NAME}" \
-o jsonpath='{.items[0].spec.clusterIP}' 2>/dev/null || true)}"
if [ -n "${_forgejo_gw_ip:-}" ]; then
_host_aliases_yaml=" hostAliases:
- ip: \"${_forgejo_gw_ip}\"
hostnames:
- \"${_forgejo_host}\""
fi
fi
_kubectl apply --namespace "$WOODPECKER_NAMESPACE" -f - <<MANIFESTS
---
apiVersion: v1
kind: Secret
metadata:
name: woodpecker-credentials
namespace: ${WOODPECKER_NAMESPACE}
type: Opaque
stringData:
WOODPECKER_AGENT_SECRET: "${WOODPECKER_AGENT_SECRET}"
WOODPECKER_FORGEJO_SECRET: "${WOODPECKER_FORGEJO_SECRET}"
WOODPECKER_FORGEJO_CLIENT: "${WOODPECKER_FORGEJO_CLIENT}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: woodpecker-server-data
namespace: ${WOODPECKER_NAMESPACE}
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${WOODPECKER_STORAGE_CLASS}
resources:
requests:
storage: ${WOODPECKER_STORAGE_SIZE}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: woodpecker-server
namespace: ${WOODPECKER_NAMESPACE}
labels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: server
app.kubernetes.io/managed-by: provisioning
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: server
template:
metadata:
labels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: server
app.kubernetes.io/managed-by: provisioning
spec:
containers:
- name: woodpecker-server
image: ${WOODPECKER_IMAGE_SERVER}
ports:
- containerPort: ${WOODPECKER_HTTP_PORT}
name: http
- containerPort: ${WOODPECKER_GRPC_PORT}
name: grpc
envFrom:
- secretRef:
name: woodpecker-credentials
env:
- name: WOODPECKER_OPEN
value: "false"
- name: WOODPECKER_ADMIN
value: "${WOODPECKER_ADMIN}"
- name: WOODPECKER_HOST
value: "https://${WOODPECKER_HOSTNAME}"
- name: WOODPECKER_FORGEJO
value: "true"
- name: WOODPECKER_FORGEJO_URL
value: "${WOODPECKER_FORGEJO_URL}"
- name: WOODPECKER_DATABASE_DRIVER
value: "postgres"
- name: WOODPECKER_DATABASE_DATASOURCE
value: "postgres://postgres:\$(POSTGRES_PASSWORD)@${WOODPECKER_DB_HOST}:5432/${WOODPECKER_DATABASE}?sslmode=disable"
- name: WOODPECKER_SERVER_ADDR
value: ":${WOODPECKER_HTTP_PORT}"
- name: WOODPECKER_GRPC_ADDR
value: ":${WOODPECKER_GRPC_PORT}"
volumeMounts:
- name: data
mountPath: /var/lib/woodpecker
readinessProbe:
httpGet:
path: /healthz
port: ${WOODPECKER_HTTP_PORT}
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: ${WOODPECKER_HTTP_PORT}
initialDelaySeconds: 30
periodSeconds: 15
${_host_aliases_yaml}
volumes:
- name: data
persistentVolumeClaim:
claimName: woodpecker-server-data
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: woodpecker-agent
namespace: ${WOODPECKER_NAMESPACE}
labels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: agent
app.kubernetes.io/managed-by: provisioning
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: agent
template:
metadata:
labels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: agent
app.kubernetes.io/managed-by: provisioning
spec:
containers:
- name: woodpecker-agent
image: ${WOODPECKER_IMAGE_AGENT}
envFrom:
- secretRef:
name: woodpecker-credentials
env:
- name: WOODPECKER_SERVER
value: "woodpecker-server.${WOODPECKER_NAMESPACE}.svc:${WOODPECKER_GRPC_PORT}"
- name: WOODPECKER_MAX_WORKFLOWS
value: "4"
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
---
apiVersion: v1
kind: Service
metadata:
name: woodpecker-server
namespace: ${WOODPECKER_NAMESPACE}
labels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: server
app.kubernetes.io/managed-by: provisioning
spec:
selector:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/component: server
ports:
- name: http
port: ${WOODPECKER_HTTP_PORT}
targetPort: ${WOODPECKER_HTTP_PORT}
- name: grpc
port: ${WOODPECKER_GRPC_PORT}
targetPort: ${WOODPECKER_GRPC_PORT}
MANIFESTS
echo "Waiting for Woodpecker server deployment to be ready..."
_wait_for_deployment "woodpecker-server" 180
echo "Setting up TLS certificate..."
_lib_ensure_certificate \
"${WOODPECKER_TLS_SECRET}" \
"${WOODPECKER_NAMESPACE}" \
"${WOODPECKER_HOSTNAME}" \
"${WOODPECKER_CLUSTER_ISSUER}"
echo "Patching gateway with HTTPS listener..."
_lib_gateway_patch_https \
"${WOODPECKER_GATEWAY_NAME}" \
"${WOODPECKER_GATEWAY_NS}" \
"https-woodpecker" \
"${WOODPECKER_HOSTNAME}" \
"${WOODPECKER_TLS_SECRET}" \
"${WOODPECKER_NAMESPACE}"
_kubectl apply --namespace "$WOODPECKER_NAMESPACE" -f - <<REFGRANT
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: woodpecker-tls-from-kube-system
namespace: ${WOODPECKER_NAMESPACE}
labels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/managed-by: provisioning
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: ${WOODPECKER_GATEWAY_NS}
to:
- group: ""
kind: Secret
name: ${WOODPECKER_TLS_SECRET}
REFGRANT
_kubectl apply --namespace "$WOODPECKER_NAMESPACE" -f - <<HTTPROUTE
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: woodpecker-https
namespace: ${WOODPECKER_NAMESPACE}
labels:
app.kubernetes.io/name: woodpecker
app.kubernetes.io/managed-by: provisioning
spec:
parentRefs:
- name: ${WOODPECKER_GATEWAY_NAME}
namespace: ${WOODPECKER_GATEWAY_NS}
sectionName: https-woodpecker
hostnames:
- "${WOODPECKER_HOSTNAME}"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: woodpecker-server
port: ${WOODPECKER_HTTP_PORT}
HTTPROUTE
echo "Woodpecker installed at https://${WOODPECKER_HOSTNAME}"
}
_do_update() {
_kubectl set image deployment/woodpecker-server \
woodpecker-server="$WOODPECKER_IMAGE_SERVER" \
--namespace "$WOODPECKER_NAMESPACE"
_kubectl set image deployment/woodpecker-agent \
woodpecker-agent="$WOODPECKER_IMAGE_AGENT" \
--namespace "$WOODPECKER_NAMESPACE"
echo "Waiting for Woodpecker server rollout..."
_wait_for_deployment "woodpecker-server" 180
echo "Woodpecker updated"
}
_do_delete() {
if _namespace_exists; then
_lib_gateway_remove_https \
"${WOODPECKER_GATEWAY_NAME}" \
"${WOODPECKER_GATEWAY_NS}" \
"https-woodpecker"
_kubectl delete namespace "$WOODPECKER_NAMESPACE" --timeout=60s
echo "Namespace $WOODPECKER_NAMESPACE deleted"
else
echo "Namespace $WOODPECKER_NAMESPACE does not exist, nothing to delete"
fi
}
_do_health() {
local pod
pod=$(_kubectl get pod --namespace "$WOODPECKER_NAMESPACE" \
--selector "app.kubernetes.io/name=woodpecker,app.kubernetes.io/component=server" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [ -z "$pod" ]; then
echo "UNHEALTHY: no woodpecker-server pod found in namespace $WOODPECKER_NAMESPACE" >&2
exit 1
fi
local status
status=$(_kubectl exec --namespace "$WOODPECKER_NAMESPACE" "$pod" -- \
wget -qO- --server-response "http://localhost:${WOODPECKER_HTTP_PORT}/healthz" 2>&1 \
| grep "HTTP/" | tail -1 | awk '{print $2}' || echo "")
if [ "$status" = "200" ]; then
echo "HEALTHY: woodpecker /healthz returned HTTP 200"
else
echo "UNHEALTHY: woodpecker /healthz returned HTTP $status" >&2
exit 1
fi
}