Adding gitea backups and exposing it to the internet read only

This commit is contained in:
juvdiaz 2026-05-25 11:42:13 -06:00
parent f25b14abfe
commit 06c963d8e6
9 changed files with 233 additions and 13 deletions

View File

@ -197,6 +197,48 @@ For the current lab, `/var/openebs/local` and `/var/lib/docker` are expected to
live on larger storage than the root filesystem. This keeps retained PVs, live on larger storage than the root filesystem. This keeps retained PVs,
container layers, Buildx state, and image caches from filling `/`. container layers, Buildx state, and image caches from filling `/`.
## Gitea
Gitea is deployed from `apps/gitea`, stores data in the retained local PV at
`/var/openebs/local/gitea`, and is exposed through the public edge path at
`https://lab2025.duckdns.org/git/`. HTTP clone and push traffic goes through the
same path. The NodePort remains available inside the lab at port `30300`.
`./lab.sh up` applies the Gitea manifests directly before creating Argo CD
Applications. This keeps the Git service bootstrap-safe if the GitOps repo is
later moved into in-cluster Gitea.
After the repo exists in Gitea, Argo CD can be pointed at the internal service
URL so it no longer depends on the old external Git server:
```bash
export TF_VAR_gitops_repo_url='http://gitea.gitea-system.svc.cluster.local:3000/jv/my-homelab-configs.git'
tofu -chdir=bootstrap/platform apply -auto-approve
tofu -chdir=bootstrap/apps apply -auto-approve
```
## Gitea Backups
`./lab.sh up` installs a Debian-host systemd timer named
`homelab-gitea-backup.timer`. The timer runs daily, executes `gitea dump` inside
the Gitea pod, copies the dump out of Kubernetes, and stores it under
`/var/backups/homelab/gitea` on the Debian server. The default retention is 30
days.
Run a manual backup from the Debian server with:
```bash
./lab.sh backup-gitea
```
Useful checks:
```bash
systemctl list-timers homelab-gitea-backup.timer
sudo systemctl start homelab-gitea-backup.service
sudo ls -lh /var/backups/homelab/gitea
```
## Destructive Rebuilds ## Destructive Rebuilds
`./lab.sh nuke` resets kubeadm, containerd runtime state, CNI files, Calico `./lab.sh nuke` resets kubeadm, containerd runtime state, CNI files, Calico

View File

@ -43,22 +43,36 @@ spec:
value: "true" value: "true"
- name: GITEA__migrations__ALLOW_LOCALNETWORKS - name: GITEA__migrations__ALLOW_LOCALNETWORKS
value: "true" value: "true"
- name: GITEA__repository__DEFAULT_PRIVATE
value: public
- name: GITEA__security__INSTALL_LOCK
value: "true"
- name: GITEA__server__DOMAIN
value: lab2025.duckdns.org
- name: GITEA__server__ROOT_URL
value: https://lab2025.duckdns.org/git/
- name: GITEA__server__SERVE_FROM_SUB_PATH
value: "true"
- name: GITEA__server__SSH_PORT - name: GITEA__server__SSH_PORT
value: "32222" value: "32222"
- name: GITEA__server__SSH_LISTEN_PORT - name: GITEA__server__SSH_LISTEN_PORT
value: "22" value: "22"
- name: GITEA__service__DISABLE_REGISTRATION
value: "true"
- name: GITEA__service__REQUIRE_SIGNIN_VIEW
value: "false"
volumeMounts: volumeMounts:
- name: gitea-data - name: gitea-data
mountPath: /data mountPath: /data
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: /git/
port: http port: http
initialDelaySeconds: 20 initialDelaySeconds: 20
periodSeconds: 10 periodSeconds: 10
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: /git/
port: http port: http
initialDelaySeconds: 60 initialDelaySeconds: 60
periodSeconds: 30 periodSeconds: 30

View File

@ -298,13 +298,13 @@ resource "null_resource" "kubeadm_worker" {
registry_config_version = "6" registry_config_version = "6"
node_dns_servers = join(" ", var.node_dns_servers) node_dns_servers = join(" ", var.node_dns_servers)
persistent_volume_dirs = join(",", var.persistent_volume_dirs) persistent_volume_dirs = join(",", var.persistent_volume_dirs)
tailscale_nodeport_version = "2" tailscale_nodeport_version = "3"
tailscale_nodeport_enabled = var.tailscale_nodeport_access.enabled && each.key == var.tailscale_nodeport_access.worker_key ? "true" : "false" tailscale_nodeport_enabled = var.tailscale_nodeport_access.enabled && each.key == var.tailscale_nodeport_access.worker_key ? "true" : "false"
tailscale_nodeport_peer_ip = var.tailscale_nodeport_access.peer_ip tailscale_nodeport_peer_ip = var.tailscale_nodeport_access.peer_ip
tailscale_nodeport_node_tailscale_ip = var.tailscale_nodeport_access.node_tailscale_ip tailscale_nodeport_node_tailscale_ip = var.tailscale_nodeport_access.node_tailscale_ip
tailscale_nodeport_pod_cidr = var.tailscale_nodeport_access.pod_cidr tailscale_nodeport_pod_cidr = var.tailscale_nodeport_access.pod_cidr
tailscale_nodeport_node_ports = join(" ", distinct(concat([var.tailscale_nodeport_access.node_port], var.tailscale_nodeport_extra_ports))) tailscale_nodeport_node_ports = join(" ", distinct(concat([var.tailscale_nodeport_access.node_port], var.tailscale_nodeport_extra_ports)))
tailscale_nodeport_target_port = tostring(var.tailscale_nodeport_access.target_port) tailscale_nodeport_target_ports = join(" ", distinct(concat([var.tailscale_nodeport_access.target_port], var.tailscale_nodeport_extra_target_ports)))
} }
connection { connection {
@ -531,7 +531,7 @@ configure_tailscale_nodeport_access() {
local node_tailscale_ip="$3" local node_tailscale_ip="$3"
local pod_cidr="$4" local pod_cidr="$4"
local node_ports="$5" local node_ports="$5"
local target_port="$6" local target_ports="$6"
if [ "$enabled" != "true" ]; then if [ "$enabled" != "true" ]; then
return 0 return 0
@ -561,12 +561,16 @@ for node_port in $node_ports; do
iptables -C INPUT -i tailscale0 -p tcp --dport "\$node_port" -j ACCEPT 2>/dev/null || iptables -C INPUT -i tailscale0 -p tcp --dport "\$node_port" -j ACCEPT 2>/dev/null ||
iptables -I INPUT 1 -i tailscale0 -p tcp --dport "\$node_port" -j ACCEPT iptables -I INPUT 1 -i tailscale0 -p tcp --dport "\$node_port" -j ACCEPT
done done
iptables -C FORWARD -i tailscale0 -d "$pod_cidr" -p tcp --dport "$target_port" -j ACCEPT 2>/dev/null || for target_port in $target_ports; do
iptables -I FORWARD 1 -i tailscale0 -d "$pod_cidr" -p tcp --dport "$target_port" -j ACCEPT iptables -C FORWARD -i tailscale0 -d "$pod_cidr" -p tcp --dport "\$target_port" -j ACCEPT 2>/dev/null ||
iptables -I FORWARD 1 -i tailscale0 -d "$pod_cidr" -p tcp --dport "\$target_port" -j ACCEPT
done
iptables -C FORWARD -s "$pod_cidr" -o tailscale0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -C FORWARD -s "$pod_cidr" -o tailscale0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null ||
iptables -I FORWARD 1 -s "$pod_cidr" -o tailscale0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT iptables -I FORWARD 1 -s "$pod_cidr" -o tailscale0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -t nat -C POSTROUTING -s 100.64.0.0/10 -d "$pod_cidr" -p tcp --dport "$target_port" -m comment --comment tailscale-nodeport-to-pods -j MASQUERADE 2>/dev/null || for target_port in $target_ports; do
iptables -t nat -I POSTROUTING 1 -s 100.64.0.0/10 -d "$pod_cidr" -p tcp --dport "$target_port" -m comment --comment tailscale-nodeport-to-pods -j MASQUERADE iptables -t nat -C POSTROUTING -s 100.64.0.0/10 -d "$pod_cidr" -p tcp --dport "\$target_port" -m comment --comment tailscale-nodeport-to-pods -j MASQUERADE 2>/dev/null ||
iptables -t nat -I POSTROUTING 1 -s 100.64.0.0/10 -d "$pod_cidr" -p tcp --dport "\$target_port" -m comment --comment tailscale-nodeport-to-pods -j MASQUERADE
done
NODEPORT_SCRIPT_EOT NODEPORT_SCRIPT_EOT
sudo chmod 0755 /usr/local/sbin/homelab-tailscale-nodeport.sh sudo chmod 0755 /usr/local/sbin/homelab-tailscale-nodeport.sh
@ -595,7 +599,7 @@ configure_tailscale_nodeport_access \
"${self.triggers.tailscale_nodeport_node_tailscale_ip}" \ "${self.triggers.tailscale_nodeport_node_tailscale_ip}" \
"${self.triggers.tailscale_nodeport_pod_cidr}" \ "${self.triggers.tailscale_nodeport_pod_cidr}" \
"${self.triggers.tailscale_nodeport_node_ports}" \ "${self.triggers.tailscale_nodeport_node_ports}" \
"${self.triggers.tailscale_nodeport_target_port}" "${self.triggers.tailscale_nodeport_target_ports}"
configure_containerd_registry "${self.triggers.registry_endpoint}" configure_containerd_registry "${self.triggers.registry_endpoint}"

View File

@ -86,5 +86,10 @@ variable "tailscale_nodeport_access" {
variable "tailscale_nodeport_extra_ports" { variable "tailscale_nodeport_extra_ports" {
type = list(number) type = list(number)
default = [30081] default = [30081, 30300]
}
variable "tailscale_nodeport_extra_target_ports" {
type = list(number)
default = [3000]
} }

View File

@ -14,6 +14,7 @@ locals {
server_name = var.server_name server_name = var.server_name
backend_host = var.backend_host backend_host = var.backend_host
demos_backend_port = var.demos_backend_port demos_backend_port = var.demos_backend_port
gitea_backend_port = var.gitea_backend_port
}) })
default_vcl = templatefile("${path.module}/templates/default.vcl.tftpl", { default_vcl = templatefile("${path.module}/templates/default.vcl.tftpl", {
backend_host = var.backend_host backend_host = var.backend_host

View File

@ -75,6 +75,31 @@ server {
return 204; return 204;
} }
location = /git {
return 301 /git/;
}
location ^~ /git/ {
limit_req zone=one burst=20 nodelay;
client_max_body_size 512m;
proxy_pass http://${backend_host}:${gitea_backend_port};
proxy_http_version 1.1;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Prefix /git;
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
proxy_redirect off;
add_header Cache-Control "no-store";
}
location ^~ /demo-apps/ { location ^~ /demo-apps/ {
limit_req zone=one burst=20 nodelay; limit_req zone=one burst=20 nodelay;

View File

@ -53,6 +53,11 @@ variable "demos_backend_port" {
default = 30081 default = 30081
} }
variable "gitea_backend_port" {
type = number
default = 30300
}
variable "haproxy_stats_user" { variable "haproxy_stats_user" {
type = string type = string
default = "admin" default = "admin"

View File

@ -281,6 +281,20 @@ resource "null_resource" "argocd_private_repo" {
set -euo pipefail set -euo pipefail
repo_url="${self.triggers.repo_url}" repo_url="${self.triggers.repo_url}"
case "$${repo_url}" in
http://*|https://*)
kubectl --kubeconfig "${self.triggers.kubeconfig_path}" -n "${self.triggers.namespace}" create secret generic "${self.triggers.secret_name}" \
--from-literal=type=git \
--from-literal=url="${self.triggers.repo_url}" \
--dry-run=client -o yaml | kubectl --kubeconfig "${self.triggers.kubeconfig_path}" apply -f -
kubectl --kubeconfig "${self.triggers.kubeconfig_path}" -n "${self.triggers.namespace}" label secret "${self.triggers.secret_name}" \
argocd.argoproj.io/secret-type=repository --overwrite
exit 0
;;
esac
repo_target="$${repo_url#ssh://}" repo_target="$${repo_url#ssh://}"
repo_target="$${repo_target#*@}" repo_target="$${repo_target#*@}"
repo_target="$${repo_target%%/*}" repo_target="$${repo_target%%/*}"
@ -335,4 +349,3 @@ resource "helm_release" "extra_tools" {
} }
} }
} }

113
lab.sh
View File

@ -410,6 +410,112 @@ wait_for_deployment_ready() {
done done
} }
apply_gitea_bootstrap_manifests() {
kubectl --kubeconfig "${KUBECONFIG}" apply -f "${REPO_ROOT}/apps/gitea/namespace.yaml"
kubectl --kubeconfig "${KUBECONFIG}" apply -f "${REPO_ROOT}/apps/gitea/storage.yaml"
kubectl --kubeconfig "${KUBECONFIG}" apply -f "${REPO_ROOT}/apps/gitea/service.yaml"
kubectl --kubeconfig "${KUBECONFIG}" apply -f "${REPO_ROOT}/apps/gitea/deployment.yaml"
wait_for_namespace gitea-system gitea 300
wait_for_namespaced_resource gitea-system deployment gitea gitea 300
wait_for_deployment_ready gitea-system gitea gitea 300
}
install_gitea_backup_timer() {
local backup_script="/usr/local/sbin/homelab-gitea-backup.sh"
sudo tee "${backup_script}" >/dev/null <<BACKUP_SCRIPT_EOT
#!/usr/bin/env bash
set -euo pipefail
KUBECONFIG_PATH="\${KUBECONFIG_PATH:-${KUBECONFIG}}"
GITEA_NAMESPACE="\${GITEA_NAMESPACE:-gitea-system}"
GITEA_SELECTOR="\${GITEA_SELECTOR:-app=gitea}"
GITEA_CONTAINER="\${GITEA_CONTAINER:-gitea}"
GITEA_BACKUP_DIR="\${GITEA_BACKUP_DIR:-/var/backups/homelab/gitea}"
GITEA_BACKUP_RETENTION_DAYS="\${GITEA_BACKUP_RETENTION_DAYS:-30}"
REMOTE_ARCHIVE="/tmp/homelab-gitea-dump.zip"
if [[ ! -s "\${KUBECONFIG_PATH}" ]]; then
echo "Skipping Gitea backup: kubeconfig \${KUBECONFIG_PATH} does not exist."
exit 0
fi
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl is required for Gitea backups." >&2
exit 1
fi
pod="\$(kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" get pods \
-l "\${GITEA_SELECTOR}" \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
if [[ -z "\${pod}" ]]; then
echo "Skipping Gitea backup: no running Gitea pod found."
exit 0
fi
timestamp="\$(date -u +%Y%m%dT%H%M%SZ)"
tmp_archive="\$(mktemp "/tmp/gitea-\${timestamp}.XXXXXX.zip")"
backup_archive="\${GITEA_BACKUP_DIR}/gitea-\${timestamp}.zip"
cleanup() {
rm -f "\${tmp_archive}"
kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" exec "\${pod}" -c "\${GITEA_CONTAINER}" -- rm -f "\${REMOTE_ARCHIVE}" >/dev/null 2>&1 || true
}
trap cleanup EXIT
kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" exec "\${pod}" -c "\${GITEA_CONTAINER}" -- rm -f "\${REMOTE_ARCHIVE}" >/dev/null 2>&1 || true
kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" exec "\${pod}" -c "\${GITEA_CONTAINER}" -- \
gitea dump -c /data/gitea/conf/app.ini --file "\${REMOTE_ARCHIVE}"
kubectl --kubeconfig "\${KUBECONFIG_PATH}" -n "\${GITEA_NAMESPACE}" cp -c "\${GITEA_CONTAINER}" \
"\${GITEA_NAMESPACE}/\${pod}:\${REMOTE_ARCHIVE}" "\${tmp_archive}"
sudo mkdir -p "\${GITEA_BACKUP_DIR}"
sudo install -m 0640 -o root -g root "\${tmp_archive}" "\${backup_archive}"
sudo find "\${GITEA_BACKUP_DIR}" -type f -name 'gitea-*.zip' -mtime +"\${GITEA_BACKUP_RETENTION_DAYS}" -delete
echo "Created \${backup_archive}"
BACKUP_SCRIPT_EOT
sudo chmod 0755 "${backup_script}"
sudo tee /etc/systemd/system/homelab-gitea-backup.service >/dev/null <<'SERVICE_EOT'
[Unit]
Description=Back up in-cluster Gitea to Debian host storage
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/homelab-gitea-backup.sh
SERVICE_EOT
sudo tee /etc/systemd/system/homelab-gitea-backup.timer >/dev/null <<'TIMER_EOT'
[Unit]
Description=Run daily Homelab Gitea backups
[Timer]
OnCalendar=*-*-* 02:35:00
RandomizedDelaySec=20m
Persistent=true
[Install]
WantedBy=timers.target
TIMER_EOT
sudo systemctl daemon-reload
sudo systemctl enable --now homelab-gitea-backup.timer >/dev/null
}
backup_gitea() {
require_debian_server "backup-gitea"
export KUBECONFIG="${KUBECONFIG_PATH}"
install_gitea_backup_timer
sudo /usr/local/sbin/homelab-gitea-backup.sh
}
recreate_pods_for_selector() { recreate_pods_for_selector() {
local namespace="$1" local namespace="$1"
local selector="$2" local selector="$2"
@ -474,6 +580,8 @@ up() {
run_tofu_stack "bootstrap/cluster" run_tofu_stack "bootstrap/cluster"
run_tofu_stack "bootstrap/platform" run_tofu_stack "bootstrap/platform"
apply_gitea_bootstrap_manifests
install_gitea_backup_timer
run_tofu_stack "bootstrap/apps" run_tofu_stack "bootstrap/apps"
refresh_argocd_application container-registry refresh_argocd_application container-registry
@ -707,11 +815,14 @@ case "${1:-}" in
up) up)
up up
;; ;;
backup-gitea)
backup_gitea
;;
nuke) nuke)
nuke nuke
;; ;;
*) *)
echo "Usage: $0 {up|nuke}" echo "Usage: $0 {up|backup-gitea|nuke}"
exit 1 exit 1
;; ;;
esac esac