382 lines
11 KiB
Bash
382 lines
11 KiB
Bash
|
|
# 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
|
||
|
|
}
|