--- # 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]