From 861d89d36ab079b40b4dc1c4f220fa57cbfc990f Mon Sep 17 00:00:00 2001 From: mr-forust Date: Sun, 6 Sep 2026 20:37:42 +0200 Subject: [PATCH] ci(deploy): split runtime by k8s/active marker services marked k8s/active are applied via kubectl; the rest via docker compose. inactive services with k8s/ keep only routing manifests (external Services, EndpointSlices, Ingresses) to reach docker backends. headscale/nextcloud routing moved to k8s/routing/. validations: compose config --quiet + kubectl apply --dry-run=client. namespace manifests applied first. pull_policy:build stacks get build+push before up so the registry image stays fresh. --- .gitea/workflows/deploy.yaml | 155 ++++++++++++++++++ .github/workflows/deploy.yaml | 155 ++++++++++++++++++ adguardhome/k8s/active | 0 authentik/k8s/active | 0 cfddns/k8s/active | 0 checkmk/k8s/active | 0 converters/k8s/active | 0 dockmon/k8s/active | 0 gitea/k8s/active | 0 glance/k8s/active | 0 .../k8s/{ => routing}/external-service.yaml | 0 headscale/k8s/{ => routing}/ingress.yaml | 0 homepages/k8s/active | 0 metube/k8s/active | 0 n8n/k8s/active | 0 netronome/k8s/active | 0 .../k8s/{ => routing}/external-service.yaml | 0 nextcloud/k8s/{ => routing}/ingress.yaml | 0 .../k8s/{ => routing}/servers-transport.yaml | 0 portainer/k8s/active | 0 prometheus-stack/k8s/active | 0 searxng/k8s/active | 0 termix/k8s/active | 0 traefik/k8s/active | 0 uptime-kuma/k8s/active | 0 userbot/k8s/active | 0 vaultwarden/k8s/active | 0 27 files changed, 310 insertions(+) create mode 100644 .gitea/workflows/deploy.yaml create mode 100644 .github/workflows/deploy.yaml create mode 100644 adguardhome/k8s/active create mode 100644 authentik/k8s/active create mode 100644 cfddns/k8s/active create mode 100644 checkmk/k8s/active create mode 100644 converters/k8s/active create mode 100644 dockmon/k8s/active create mode 100644 gitea/k8s/active create mode 100644 glance/k8s/active rename headscale/k8s/{ => routing}/external-service.yaml (100%) rename headscale/k8s/{ => routing}/ingress.yaml (100%) create mode 100644 homepages/k8s/active create mode 100644 metube/k8s/active create mode 100644 n8n/k8s/active create mode 100644 netronome/k8s/active rename nextcloud/k8s/{ => routing}/external-service.yaml (100%) rename nextcloud/k8s/{ => routing}/ingress.yaml (100%) rename nextcloud/k8s/{ => routing}/servers-transport.yaml (100%) create mode 100644 portainer/k8s/active create mode 100644 prometheus-stack/k8s/active create mode 100644 searxng/k8s/active create mode 100644 termix/k8s/active create mode 100644 traefik/k8s/active create mode 100644 uptime-kuma/k8s/active create mode 100644 userbot/k8s/active create mode 100644 vaultwarden/k8s/active diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..d04426e --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,155 @@ +name: deploy + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-main + cancel-in-progress: false + +jobs: + redeploy: + runs-on: [self-hosted, linux, arch, homelab, prod] + steps: + - name: Redeploy workstation + shell: bash + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + # Set APPLY_PRUNE=true to enable kubectl apply --prune. Requires every + # manifest to carry label app.kubernetes.io/managed-by=homelab-deploy, + # otherwise previously applied resources get deleted on the next run. + APPLY_PRUNE: ${{ vars.APPLY_PRUNE }} + run: | + set -euo pipefail + + : "${DEPLOY_HOST:?missing DEPLOY_HOST}" + : "${DEPLOY_USER:?missing DEPLOY_USER}" + : "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}" + + deploy_port="${DEPLOY_PORT:-22}" + deploy_path="${DEPLOY_PATH:-/srv/homelab}" + + ssh_key="$RUNNER_TEMP/deploy_key" + mkdir -p "$RUNNER_TEMP" + printf '%s\n' "$DEPLOY_KEY" > "$ssh_key" + chmod 600 "$ssh_key" + + ssh_opts=( + -i "$ssh_key" + -p "$deploy_port" + -o BatchMode=yes + -o StrictHostKeyChecking=accept-new + ) + + ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \ + "DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF' + set -euo pipefail + + repo="${DEPLOY_PATH:-/srv/homelab}" + + if [ ! -d "$repo/.git" ]; then + echo "Repository not found at $repo" + exit 1 + fi + + git -C "$repo" fetch origin main + git -C "$repo" reset --hard origin/main + + # Runtime selection: a service is k8s-managed when $SERVICE/k8s/active + # exists. Otherwise it is compose-managed, and only k8s/routing/* + # manifests (external Services / EndpointSlices / ServersTransport / + # Ingresses that route to docker backends) are applied. + # migrate: touch SERVICE/k8s/active (+ move routing files up) + # rollback: rm SERVICE/k8s/active + collect_k8s() { + find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \ + ! -path '*/routing/*' ! -path '*/overlays/*' \ + ! -name 'kustomization.y*ml' ! -name '*.example.y*ml' \ + ! -name '*values.y*ml' ! -name 'patch-*.y*ml' \ + | sort + } + + collect_k8s_inactive() { + find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \ + \( -name 'namespace.y*ml' -o -path '*/routing/*' \) \ + ! -path '*/overlays/*' ! -name '*.example.y*ml' \ + | sort + } + + mapfile -t compose_stacks < <( + find "$repo" -type f \( -name 'compose.yaml' -o -name 'compose.yml' \) | sort + ) + + mapfile -t k8s_manifests < <( + for kd in $(find "$repo" -type d -name k8s ! -path '*/.git/*' | sort); do + if [ -f "$kd/active" ]; then + collect_k8s "$kd" + else + collect_k8s_inactive "$kd" + fi + done + ) + + echo "== Validate compose stacks ==" + for cf in "${compose_stacks[@]}"; do + dir=$(dirname "$cf") + if [ -f "$dir/k8s/active" ]; then + echo " skip (k8s-managed): $dir" + continue + fi + echo " config: $cf" + docker compose -f "$cf" config --quiet + done + + echo "== Validate k8s manifests (kubectl dry-run) ==" + for m in "${k8s_manifests[@]}"; do + echo " apply --dry-run=client $m" + kubectl apply --dry-run=client -f "$m" >/dev/null + done + + echo "== Applying Kubernetes manifests ==" + ns_files=() + other_files=() + for m in "${k8s_manifests[@]}"; do + case "$m" in + */namespace.y?ml) ns_files+=("$m") ;; + *) other_files+=("$m") ;; + esac + done + + prune_opts=() + if [ "${APPLY_PRUNE:-false}" = "true" ]; then + prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy) + fi + + if [ "${#ns_files[@]}" -gt 0 ]; then + echo " namespaces first: ${ns_files[*]}" + kubectl apply -f "${ns_files[@]}" + fi + if [ "${#other_files[@]}" -gt 0 ]; then + echo " resources: ${other_files[*]}" + kubectl apply "${prune_opts[@]}" -f "${other_files[@]}" + fi + + echo "== Redeploying docker compose stacks ==" + for cf in "${compose_stacks[@]}"; do + dir=$(dirname "$cf") + if [ -f "$dir/k8s/active" ]; then + echo " skip (k8s-managed): $dir" + continue + fi + echo " compose: $dir" + if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then + docker compose -f "$cf" build + docker compose -f "$cf" push + fi + docker compose -f "$cf" up -d --pull always --remove-orphans + done + EOF \ No newline at end of file diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..d04426e --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,155 @@ +name: deploy + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: deploy-main + cancel-in-progress: false + +jobs: + redeploy: + runs-on: [self-hosted, linux, arch, homelab, prod] + steps: + - name: Redeploy workstation + shell: bash + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + # Set APPLY_PRUNE=true to enable kubectl apply --prune. Requires every + # manifest to carry label app.kubernetes.io/managed-by=homelab-deploy, + # otherwise previously applied resources get deleted on the next run. + APPLY_PRUNE: ${{ vars.APPLY_PRUNE }} + run: | + set -euo pipefail + + : "${DEPLOY_HOST:?missing DEPLOY_HOST}" + : "${DEPLOY_USER:?missing DEPLOY_USER}" + : "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}" + + deploy_port="${DEPLOY_PORT:-22}" + deploy_path="${DEPLOY_PATH:-/srv/homelab}" + + ssh_key="$RUNNER_TEMP/deploy_key" + mkdir -p "$RUNNER_TEMP" + printf '%s\n' "$DEPLOY_KEY" > "$ssh_key" + chmod 600 "$ssh_key" + + ssh_opts=( + -i "$ssh_key" + -p "$deploy_port" + -o BatchMode=yes + -o StrictHostKeyChecking=accept-new + ) + + ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \ + "DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF' + set -euo pipefail + + repo="${DEPLOY_PATH:-/srv/homelab}" + + if [ ! -d "$repo/.git" ]; then + echo "Repository not found at $repo" + exit 1 + fi + + git -C "$repo" fetch origin main + git -C "$repo" reset --hard origin/main + + # Runtime selection: a service is k8s-managed when $SERVICE/k8s/active + # exists. Otherwise it is compose-managed, and only k8s/routing/* + # manifests (external Services / EndpointSlices / ServersTransport / + # Ingresses that route to docker backends) are applied. + # migrate: touch SERVICE/k8s/active (+ move routing files up) + # rollback: rm SERVICE/k8s/active + collect_k8s() { + find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \ + ! -path '*/routing/*' ! -path '*/overlays/*' \ + ! -name 'kustomization.y*ml' ! -name '*.example.y*ml' \ + ! -name '*values.y*ml' ! -name 'patch-*.y*ml' \ + | sort + } + + collect_k8s_inactive() { + find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \ + \( -name 'namespace.y*ml' -o -path '*/routing/*' \) \ + ! -path '*/overlays/*' ! -name '*.example.y*ml' \ + | sort + } + + mapfile -t compose_stacks < <( + find "$repo" -type f \( -name 'compose.yaml' -o -name 'compose.yml' \) | sort + ) + + mapfile -t k8s_manifests < <( + for kd in $(find "$repo" -type d -name k8s ! -path '*/.git/*' | sort); do + if [ -f "$kd/active" ]; then + collect_k8s "$kd" + else + collect_k8s_inactive "$kd" + fi + done + ) + + echo "== Validate compose stacks ==" + for cf in "${compose_stacks[@]}"; do + dir=$(dirname "$cf") + if [ -f "$dir/k8s/active" ]; then + echo " skip (k8s-managed): $dir" + continue + fi + echo " config: $cf" + docker compose -f "$cf" config --quiet + done + + echo "== Validate k8s manifests (kubectl dry-run) ==" + for m in "${k8s_manifests[@]}"; do + echo " apply --dry-run=client $m" + kubectl apply --dry-run=client -f "$m" >/dev/null + done + + echo "== Applying Kubernetes manifests ==" + ns_files=() + other_files=() + for m in "${k8s_manifests[@]}"; do + case "$m" in + */namespace.y?ml) ns_files+=("$m") ;; + *) other_files+=("$m") ;; + esac + done + + prune_opts=() + if [ "${APPLY_PRUNE:-false}" = "true" ]; then + prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy) + fi + + if [ "${#ns_files[@]}" -gt 0 ]; then + echo " namespaces first: ${ns_files[*]}" + kubectl apply -f "${ns_files[@]}" + fi + if [ "${#other_files[@]}" -gt 0 ]; then + echo " resources: ${other_files[*]}" + kubectl apply "${prune_opts[@]}" -f "${other_files[@]}" + fi + + echo "== Redeploying docker compose stacks ==" + for cf in "${compose_stacks[@]}"; do + dir=$(dirname "$cf") + if [ -f "$dir/k8s/active" ]; then + echo " skip (k8s-managed): $dir" + continue + fi + echo " compose: $dir" + if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then + docker compose -f "$cf" build + docker compose -f "$cf" push + fi + docker compose -f "$cf" up -d --pull always --remove-orphans + done + EOF \ No newline at end of file diff --git a/adguardhome/k8s/active b/adguardhome/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/authentik/k8s/active b/authentik/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/cfddns/k8s/active b/cfddns/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/checkmk/k8s/active b/checkmk/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/converters/k8s/active b/converters/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/dockmon/k8s/active b/dockmon/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/gitea/k8s/active b/gitea/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/glance/k8s/active b/glance/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/headscale/k8s/external-service.yaml b/headscale/k8s/routing/external-service.yaml similarity index 100% rename from headscale/k8s/external-service.yaml rename to headscale/k8s/routing/external-service.yaml diff --git a/headscale/k8s/ingress.yaml b/headscale/k8s/routing/ingress.yaml similarity index 100% rename from headscale/k8s/ingress.yaml rename to headscale/k8s/routing/ingress.yaml diff --git a/homepages/k8s/active b/homepages/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/metube/k8s/active b/metube/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/n8n/k8s/active b/n8n/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/netronome/k8s/active b/netronome/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/nextcloud/k8s/external-service.yaml b/nextcloud/k8s/routing/external-service.yaml similarity index 100% rename from nextcloud/k8s/external-service.yaml rename to nextcloud/k8s/routing/external-service.yaml diff --git a/nextcloud/k8s/ingress.yaml b/nextcloud/k8s/routing/ingress.yaml similarity index 100% rename from nextcloud/k8s/ingress.yaml rename to nextcloud/k8s/routing/ingress.yaml diff --git a/nextcloud/k8s/servers-transport.yaml b/nextcloud/k8s/routing/servers-transport.yaml similarity index 100% rename from nextcloud/k8s/servers-transport.yaml rename to nextcloud/k8s/routing/servers-transport.yaml diff --git a/portainer/k8s/active b/portainer/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/prometheus-stack/k8s/active b/prometheus-stack/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/searxng/k8s/active b/searxng/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/termix/k8s/active b/termix/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/traefik/k8s/active b/traefik/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/uptime-kuma/k8s/active b/uptime-kuma/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/userbot/k8s/active b/userbot/k8s/active new file mode 100644 index 0000000..e69de29 diff --git a/vaultwarden/k8s/active b/vaultwarden/k8s/active new file mode 100644 index 0000000..e69de29