159 lines
6.3 KiB
YAML

---
# playbooks/docker/deploy_gitea.yml
#
# Purpose:
# Deploy Gitea as a Swarm stack pinned to swarm-manager-1, with a dedicated
# Postgres sidecar and persistent bind mounts under /mnt/homelab/apps/gitea.
#
# Data protection:
# Preflight checks require all data paths to exist before deploy.
# If paths are missing, deployment fails early to avoid creating an empty
# data root over an existing Gitea installation.
#
# Usage:
# ansible-playbook -i inventory/hosts.ini playbooks/docker/deploy_gitea.yml
#
# Validate only (no Swarm mutations):
# ansible-playbook -i inventory/hosts.ini playbooks/docker/deploy_gitea.yml \
# -e "stack_validate_only=true"
#
# Tear down:
# ansible-playbook -i inventory/hosts.ini playbooks/docker/deploy_gitea.yml \
# -e "gitea_deploy_state=absent"
- name: Deploy Gitea Swarm stack
hosts: swarm_managers
become: true
gather_facts: false
vars_files:
- ../../group_vars/all.yml
vars:
gitea_deploy_target: "{{ edge_routing.swarm.stack_deploy_target | default(groups['swarm_managers'][0]) }}"
tasks:
# --------------------------------------------------
# STEP 0: Assert required secrets are present
# --------------------------------------------------
- name: Assert vault_gitea_db_password is defined and non-empty
ansible.builtin.assert:
that:
- vault_gitea_db_password is defined
- vault_gitea_db_password | trim | length > 0
fail_msg: >-
vault_gitea_db_password is not defined or is empty.
Encrypt and store it in group_vars/vault/all.yml with:
ansible-vault encrypt_string 'your-db-password' --name 'vault_gitea_db_password'
when: inventory_hostname == gitea_deploy_target
- name: Assert vault_gitea_db_password is not a placeholder
ansible.builtin.assert:
that:
- vault_gitea_db_password not in ['change-me', 'changeme', 'your-db-password']
fail_msg: "vault_gitea_db_password still appears to be a placeholder. Set a real vault value before deploy."
when: inventory_hostname == gitea_deploy_target
# --------------------------------------------------
# STEP 1: Assert Swarm manager is active
# WHY exact equality: search('active') matches 'inactive' as a substring.
# The format string yields 'active|true' only for a healthy manager.
# --------------------------------------------------
- name: Collect Swarm manager state
ansible.builtin.command: >
docker info --format '{{ "{{" }}.Swarm.LocalNodeState{{ "}}" }}|{{ "{{" }}.Swarm.ControlAvailable{{ "}}" }}'
register: _swarm_info
changed_when: false
when: inventory_hostname == gitea_deploy_target
- name: Assert target is an active Swarm manager
ansible.builtin.assert:
that:
- _swarm_info.stdout == 'active|true'
fail_msg: >-
{{ inventory_hostname }} must be an active Swarm manager.
Expected 'active|true', got '{{ _swarm_info.stdout | default('unknown') }}'.
when: inventory_hostname == gitea_deploy_target
# --------------------------------------------------
# STEP 2: Validate pre-existing persistent data paths
# WHY: Missing paths cause Gitea to bootstrap a fresh install over existing
# data. The operator must create or restore paths before deploying.
# --------------------------------------------------
- name: Stat required Gitea bind-mount paths
ansible.builtin.stat:
path: "{{ item }}"
register: _gitea_path_stat
loop:
- /mnt/homelab/apps/gitea
- /mnt/homelab/apps/gitea/data
- /mnt/homelab/apps/gitea/data/db
when: inventory_hostname == gitea_deploy_target
- name: Assert required Gitea paths exist before deploy
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.isdir
fail_msg: >-
Required Gitea path '{{ item.item }}' is missing on {{ inventory_hostname }}.
Create or restore this directory first to protect existing data.
loop: "{{ _gitea_path_stat.results }}"
when: inventory_hostname == gitea_deploy_target
# --------------------------------------------------
# STEP 3: Deploy Gitea stack
# --------------------------------------------------
- name: Deploy Gitea stack
ansible.builtin.include_role:
name: swarm_stack_deploy
vars:
stack_name: "gitea"
stack_compose_src: "{{ playbook_dir }}/../../templates/stacks/gitea.stack.yml"
# WHY gitea_deploy_state (not stack_state): using stack_state directly
# creates a Jinja2 self-reference loop inside the role.
stack_state: "{{ gitea_deploy_state | default('present') }}"
stack_required_external_networks:
- proxy-net
stack_required_directories:
- /mnt/homelab/apps/gitea
- /mnt/homelab/apps/gitea/data
- /mnt/homelab/apps/gitea/data/db
when: inventory_hostname == gitea_deploy_target
# --------------------------------------------------
# STEP 4: Wait for service convergence
# --------------------------------------------------
- name: Wait for Gitea server service to converge
ansible.builtin.command: >
docker service ls --filter name=gitea_server --format '{{ "{{" }}.Replicas{{ "}}" }}'
register: _gitea_replicas
retries: 18
delay: 10
until: _gitea_replicas.stdout is search('1/1')
changed_when: false
when:
- inventory_hostname == gitea_deploy_target
- gitea_deploy_state | default('present') == 'present'
- not ansible_check_mode
tags: [verify]
- name: Report Gitea deployment result
ansible.builtin.debug:
msg:
- "================================================"
- "Gitea deployment complete."
- "================================================"
- "Stack : gitea"
- "Manager : {{ inventory_hostname }} ({{ ansible_host | default('') }})"
- "URL : https://git.castaldifamily.com"
- "Data root : /mnt/homelab/apps/gitea"
- "Services : gitea_server, gitea_gitea-db"
- "================================================"
when: inventory_hostname == gitea_deploy_target
tags: [always]