306 lines
12 KiB
Bash
306 lines
12 KiB
Bash
|
|
# 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)"
|
||
|
|
}
|