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

306 lines
12 KiB
Bash
Raw Permalink Normal View History

# Methods library for radicle_seed — sourced by install-radicle_seed.sh (and the
# orchestrator's generated run-*.sh). SCRIPT_DIR and the RADICLE_* env must be set
# before sourcing. Mirrors the forgejo-lib.sh structure.
#
# Multi-instance: RADICLE_NAME drives all resource names so radicle-src and
# radicle-pub can coexist with radicle-seed on the same cluster.
_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
}
_render_config_json() {
local policy
if [ "${RADICLE_SEEDING_DEFAULT}" = "allow" ]; then
policy="\"seedingPolicy\": { \"default\": \"allow\", \"scope\": \"${RADICLE_SEEDING_SCOPE}\" }"
else
policy="\"seedingPolicy\": { \"default\": \"block\" }"
fi
# Build the connect array as JSON from the space-separated peer list — pure
# bash construction (no parser needed): quote each peer, comma-join.
local connect_json="[]"
if [ -n "${RADICLE_NODE_CONNECTS:-}" ]; then
connect_json="["
local _sep="" _peer
for _peer in ${RADICLE_NODE_CONNECTS}; do
connect_json="${connect_json}${_sep}\"${_peer}\""
_sep=","
done
connect_json="${connect_json}]"
fi
cat <<JSON
{
"node": {
"alias": "${RADICLE_NAME}",
"listen": ["0.0.0.0:${RADICLE_P2P_PORT}"],
"externalAddresses": ["${RADICLE_EXTERNAL_ADDR}"],
"connect": ${connect_json},
${policy}
}
}
JSON
}
_apply_namespace() {
_kubectl create namespace "$RADICLE_NAMESPACE" --dry-run=client -o yaml | _kubectl apply -f -
}
_apply_identity_secret() {
[ -n "${RADICLE_IDENTITY_KEY_B64:-}" ] || { echo "ERROR: RADICLE_IDENTITY_KEY_B64 unset — _credentials.env (SOPS) not injected" >&2; exit 1; }
local kf pf
kf="$(mktemp)"; pf="$(mktemp)"
printf '%s' "$RADICLE_IDENTITY_KEY_B64" | base64 -d > "$kf"
printf '%s' "$RADICLE_IDENTITY_PUB_B64" | base64 -d > "$pf"
_kubectl create secret generic "${RADICLE_NAME}-identity" \
--namespace "$RADICLE_NAMESPACE" \
--from-file=radicle="$kf" \
--from-file=radicle.pub="$pf" \
--dry-run=client -o yaml | _kubectl apply -f -
rm -f "$kf" "$pf"
}
_apply_config() {
_kubectl create configmap "${RADICLE_NAME}-config" \
--namespace "$RADICLE_NAMESPACE" \
--from-literal=config.json="$(_render_config_json)" \
--dry-run=client -o yaml | _kubectl apply -f -
}
_hostnet_spec() {
# Emit hostNetwork/dnsPolicy/nodeSelector lines only for VPN-only seeds that need
# to be reachable at the node's wuwei IP without a LoadBalancer Service.
# RADICLE_HOST_NETWORK=true + RADICLE_NODE_SELECTOR=<hostname> required together.
[ "${RADICLE_HOST_NETWORK:-false}" = "true" ] || return 0
echo " hostNetwork: true"
echo " dnsPolicy: ClusterFirstWithHostNet"
[ -n "${RADICLE_NODE_SELECTOR:-}" ] && echo " nodeSelector:"
[ -n "${RADICLE_NODE_SELECTOR:-}" ] && echo " kubernetes.io/hostname: ${RADICLE_NODE_SELECTOR}"
}
_apply_statefulset() {
_kubectl apply -f - <<MANIFEST
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ${RADICLE_NAME}
namespace: ${RADICLE_NAMESPACE}
labels: { app: ${RADICLE_NAME} }
spec:
serviceName: ${RADICLE_NAME}
replicas: 1
selector:
matchLabels: { app: ${RADICLE_NAME} }
template:
metadata:
labels: { app: ${RADICLE_NAME} }
spec:
$(_hostnet_spec)
initContainers:
- name: init-home
image: ${RADICLE_IMAGE}
command: ["/bin/sh", "-c"]
securityContext:
runAsUser: 0
args:
- |
set -eu
mkdir -p "\$RAD_HOME/keys"
install -m0600 /identity/radicle "\$RAD_HOME/keys/radicle"
install -m0644 /identity/radicle.pub "\$RAD_HOME/keys/radicle.pub"
install -m0644 /config/config.json "\$RAD_HOME/config.json"
chown -R 1000:1000 /radicle
env:
- { name: RAD_HOME, value: ${RADICLE_RAD_HOME} }
volumeMounts:
- { name: home, mountPath: /radicle }
- { name: identity, mountPath: /identity, readOnly: true }
- { name: config, mountPath: /config, readOnly: true }
containers:
- name: radicle-node
image: ${RADICLE_IMAGE}
command: ["radicle-node", "--listen", "0.0.0.0:${RADICLE_P2P_PORT}"]
env:
- { name: RAD_HOME, value: ${RADICLE_RAD_HOME} }
$([ "${RADICLE_HOST_NETWORK:-false}" != "true" ] && echo " ports:" && echo " - { containerPort: ${RADICLE_P2P_PORT}, name: p2p }")
volumeMounts:
- { name: home, mountPath: /radicle }
- name: radicle-httpd
image: ${RADICLE_IMAGE}
command: ["radicle-httpd", "--listen", "0.0.0.0:${RADICLE_HTTP_PORT}"]
env:
- { name: RAD_HOME, value: ${RADICLE_RAD_HOME} }
ports:
- { containerPort: ${RADICLE_HTTP_PORT}, name: http }
volumeMounts:
- { name: home, mountPath: /radicle }
volumes:
- name: identity
secret:
secretName: ${RADICLE_NAME}-identity
defaultMode: 0600
- name: config
configMap:
name: ${RADICLE_NAME}-config
volumeClaimTemplates:
- metadata:
name: home
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ${RADICLE_STORAGE_CLASS}
resources:
requests:
storage: ${RADICLE_STORAGE_SIZE}
MANIFEST
}
_apply_service() {
# When hostNetwork=true (VPN-only seeds), omit the P2P port from the ClusterIP
# Service. Cilium's socket-LB maps backend IPs to service entries; exposing the
# P2P port causes Cilium to intercept return traffic from external peers and drop
# it with "Service backend not found" because the conntrack state doesn't match.
local p2p_port_entry
if [ "${RADICLE_HOST_NETWORK:-false}" = "true" ]; then
p2p_port_entry=""
else
p2p_port_entry=" - { name: p2p, port: ${RADICLE_P2P_PORT}, targetPort: ${RADICLE_P2P_PORT} }"
fi
_kubectl apply -f - <<MANIFEST
apiVersion: v1
kind: Service
metadata:
name: ${RADICLE_NAME}
namespace: ${RADICLE_NAMESPACE}
labels: { app: ${RADICLE_NAME} }
spec:
selector: { app: ${RADICLE_NAME} }
ports:
${p2p_port_entry}
- { name: http, port: ${RADICLE_HTTP_PORT}, targetPort: ${RADICLE_HTTP_PORT} }
MANIFEST
}
_wait() {
_kubectl rollout status "statefulset/${RADICLE_NAME}" \
--namespace "$RADICLE_NAMESPACE" \
--timeout="${1:-300}s"
}
# HTTPS for radicle-httpd: attach to the shared `libre-daoshi` Cilium gateway via
# the canonical gateway-tls.sh lib — upsert an HTTPS listener + cert-manager
# Certificate, then route the hostname to httpd.
_apply_gateway() {
[ "${RADICLE_GATEWAY_ENABLED}" = "true" ] || { echo "[${RADICLE_NAME}] HTTPS gateway disabled"; return 0; }
[ -f "${SCRIPT_DIR}/lib/gateway-tls.sh" ] || { echo "ERROR: lib/gateway-tls.sh missing from bundle" >&2; exit 1; }
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib/gateway-tls.sh"
_lib_ensure_certificate "${RADICLE_CERT_SECRET}" "${RADICLE_CERT_NAMESPACE}" \
"${RADICLE_HOSTNAME}" "${RADICLE_CLUSTER_ISSUER}"
_lib_gateway_patch_https "${RADICLE_GATEWAY_NAME}" "${RADICLE_GATEWAY_NS}" \
"${RADICLE_LISTENER_NAME}" "${RADICLE_HOSTNAME}" "${RADICLE_CERT_SECRET}" "${RADICLE_CERT_NAMESPACE}"
_kubectl apply -f - <<ROUTE
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: ${RADICLE_NAME}-https
namespace: ${RADICLE_NAMESPACE}
labels: { app.kubernetes.io/name: ${RADICLE_NAME}, app.kubernetes.io/managed-by: provisioning }
spec:
parentRefs:
- name: ${RADICLE_GATEWAY_NAME}
namespace: ${RADICLE_GATEWAY_NS}
sectionName: ${RADICLE_LISTENER_NAME}
hostnames:
- "${RADICLE_HOSTNAME}"
rules:
- matches:
- path: { type: PathPrefix, value: / }
backendRefs:
- name: ${RADICLE_NAME}
port: ${RADICLE_HTTP_PORT}
ROUTE
}
# TCP for radicle-node P2P: add a TCP listener to the shared Gateway and create
# a TCPRoute forwarding that port to the ClusterIP Service. Only wired for
# visibility=unlisted and visibility=public nodes (tcp_gateway.enabled=true).
_apply_tcp_gateway() {
[ "${RADICLE_TCP_GATEWAY_ENABLED:-false}" = "true" ] || { echo "[${RADICLE_NAME}] TCP gateway disabled"; return 0; }
[ -n "${RADICLE_TCP_LISTENER_NAME}" ] || { echo "ERROR: RADICLE_TCP_LISTENER_NAME unset" >&2; exit 1; }
[ "${RADICLE_TCP_PORT:-0}" -gt 0 ] || { echo "ERROR: RADICLE_TCP_PORT unset or zero" >&2; exit 1; }
[ -f "${SCRIPT_DIR}/lib/gateway-tcp.sh" ] || { echo "ERROR: lib/gateway-tcp.sh missing from bundle" >&2; exit 1; }
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib/gateway-tcp.sh"
_lib_gateway_patch_tcp "${RADICLE_GATEWAY_NAME}" "${RADICLE_GATEWAY_NS}" \
"${RADICLE_TCP_LISTENER_NAME}" "${RADICLE_TCP_PORT}"
_lib_upsert_tcproute "${RADICLE_NAME}-p2p" "${RADICLE_NAMESPACE}" \
"${RADICLE_GATEWAY_NAME}" "${RADICLE_GATEWAY_NS}" "${RADICLE_TCP_LISTENER_NAME}" \
"${RADICLE_NAME}" "${RADICLE_TCP_PORT}"
}
# Selective seeding: with default=block, explicitly seed each allowed RID.
_seed_rids() {
[ -n "${RADICLE_SEED_RIDS}" ] || { echo "[${RADICLE_NAME}] no RIDs to seed (allow-list empty)"; return 0; }
for rid in ${RADICLE_SEED_RIDS}; do
echo "[${RADICLE_NAME}] rad seed ${rid} --scope ${RADICLE_SEEDING_SCOPE}"
_kubectl exec -n "$RADICLE_NAMESPACE" "statefulset/${RADICLE_NAME}" -c radicle-node -- \
env RAD_HOME="${RADICLE_RAD_HOME}" rad seed "${rid}" --scope "${RADICLE_SEEDING_SCOPE}" || \
echo " (warn) seed ${rid} failed — node may still be starting"
done
}
cmd_install() {
echo "[${RADICLE_NAME}] namespace ${RADICLE_NAMESPACE}"
_apply_namespace
echo "[${RADICLE_NAME}] identity secret"
_apply_identity_secret
echo "[${RADICLE_NAME}] config.json (seedingPolicy=${RADICLE_SEEDING_DEFAULT}/${RADICLE_SEEDING_SCOPE}, connects=$(echo "${RADICLE_NODE_CONNECTS:-none}" | wc -w | tr -d ' '))"
_apply_config
echo "[${RADICLE_NAME}] StatefulSet (${RADICLE_IMAGE})"
_apply_statefulset
echo "[${RADICLE_NAME}] Service (p2p ${RADICLE_P2P_PORT}, http ${RADICLE_HTTP_PORT})"
_apply_service
echo "[${RADICLE_NAME}] HTTPS gateway (${RADICLE_HOSTNAME})"
_apply_gateway
echo "[${RADICLE_NAME}] TCP gateway (P2P port ${RADICLE_TCP_PORT:-disabled})"
_apply_tcp_gateway
echo "[${RADICLE_NAME}] waiting for rollout..."
_wait
echo "[${RADICLE_NAME}] selective seeding"
_seed_rids
echo "[${RADICLE_NAME}] node id: $(_kubectl exec -n "$RADICLE_NAMESPACE" "statefulset/${RADICLE_NAME}" -c radicle-node -- env RAD_HOME=${RADICLE_RAD_HOME} rad self --nid 2>/dev/null || echo '<pending>')"
}
cmd_uninstall() {
if [ "${RADICLE_TCP_GATEWAY_ENABLED:-false}" = "true" ] && [ -f "${SCRIPT_DIR}/lib/gateway-tcp.sh" ]; then
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib/gateway-tcp.sh"
_lib_delete_tcproute "${RADICLE_NAME}-p2p" "${RADICLE_NAMESPACE}"
_lib_gateway_remove_tcp "${RADICLE_GATEWAY_NAME}" "${RADICLE_GATEWAY_NS}" "${RADICLE_TCP_LISTENER_NAME}"
fi
if [ "${RADICLE_GATEWAY_ENABLED}" = "true" ] && [ -f "${SCRIPT_DIR}/lib/gateway-tcp.sh" ]; then
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib/gateway-tls.sh"
_kubectl delete "httproute/${RADICLE_NAME}-https" --namespace "$RADICLE_NAMESPACE" --ignore-not-found
_lib_gateway_remove_https "${RADICLE_GATEWAY_NAME}" "${RADICLE_GATEWAY_NS}" "${RADICLE_LISTENER_NAME}"
_kubectl delete "certificate/${RADICLE_CERT_SECRET}" --namespace "$RADICLE_CERT_NAMESPACE" --ignore-not-found
fi
_kubectl delete \
"statefulset/${RADICLE_NAME}" \
"service/${RADICLE_NAME}" \
"configmap/${RADICLE_NAME}-config" \
"secret/${RADICLE_NAME}-identity" \
--namespace "$RADICLE_NAMESPACE" --ignore-not-found
echo "[${RADICLE_NAME}] removed (PVC retained by storage class policy)"
}