---
# ============================================================================
# install-exec-node.yml
# ============================================================================
# Run this ON the execution/hop node after extracting the generated bundle.
# This playbook is shipped inside the tarball created by
# generate-exec-node-bundle.yml
#
# Usage:
#   ansible-playbook install-exec-node.yml -i inventory.yml
#
# Prerequisites:
#   - RHEL 8.6+ or RHEL 9.2+
#   - podman installed (this playbook will install it if missing)
#   - ansible-core installed
#   - Port 27199 (or configured receptor_port) open inbound for receptor
#
# Volume layout (matches AAP 2.6 containerized installer):
#   Named podman volumes are used for runtime/data dirs (receptor_run,
#   receptor_runner, receptor_home, receptor_data), exactly as the installer
#   does in roles/receptor/tasks/volumes.yml and facts.yml.
#   Config/cert files are bind-mounted read-only from ~/aap/receptor/etc/,
#   and the CA trust extracted/ dir is bind-mounted from ~/aap/tls/extracted/.
# ============================================================================

- name: "Install receptor on execution/hop node"
  hosts: all
  gather_facts: true

  vars:
    # These are set in inventory.yml (generated by generate-exec-node-bundle.yml)
    # exec_node_hostname: from inventory
    # exec_node_type:     from inventory
    # controller_hostname: from inventory
    # receptor_port:      from inventory
    # receptor_protocol:  from inventory
    # receptor_image:     from inventory
    # container_log_driver: from inventory
    # bundle_dir:         from inventory (defaults to playbook_dir)

    # AAP base directory — matches the installer default (aap_volumes_dir)
    aap_base_dir: "{{ ansible_user_dir }}/aap"

    # Receptor config directory on the host — bind-mounted read-only into container
    # Matches receptor_conf_dir in the installer's receptor/defaults/main.yml
    receptor_conf_dir: "{{ aap_base_dir }}/receptor/etc"

    # CA trust directory — the extracted/ subdir is bind-mounted into the container
    # so the mesh CA cert is trusted for HTTPS calls inside the container
    ca_tls_dir: "{{ aap_base_dir }}/tls"

    # Named podman volume names (must match installer's volumes.yml)
    receptor_run_volume: "receptor_run"       # → /run/receptor inside container
    receptor_runner_volume: "receptor_runner" # → /home/runner inside container
    receptor_home_volume: "receptor_home"     # → /home/<user> inside container
    receptor_data_volume: "receptor_data"     # → /var/lib/receptor inside container

    # Container name — the installer always names this 'receptor'
    receptor_container_name: "receptor"

    # Custom podman binary (matches installer's executionplane.yml)
    # The installer ships a wrapper podman at ~/aap/containers/podman
    aap_podman_path: "{{ aap_base_dir }}/containers/podman"

  pre_tasks:

    - name: Validate bundle files exist
      ansible.builtin.stat:
        path: "{{ bundle_dir | default(playbook_dir) }}/{{ item }}"
      register: _bundle_files
      loop:
        - certs/receptor.crt
        - certs/receptor.key
        - certs/mesh-CA.crt
        - certs/signing_public.pem
        - receptor.conf
        - images/receptor-rhel9.tar.gz
        - ca-trust-extracted.tar.gz

    - name: Fail if bundle files are missing
      ansible.builtin.fail:
        msg: >-
          Missing bundle file: {{ item.item }}.
          Ensure you extracted the tarball correctly.
      when: not item.stat.exists
      loop: "{{ _bundle_files.results }}"
      loop_control:
        label: "{{ item.item }}"

  tasks:

    # ------------------------------------------------------------------
    # 1. Install prerequisites
    # ------------------------------------------------------------------
    - name: Ensure podman is installed
      ansible.builtin.package:
        name: podman
        state: present
      become: true

    - name: Enable lingering for the service user (containers survive logout)
      ansible.builtin.command:
        cmd: "loginctl enable-linger {{ ansible_user_id }}"
      changed_when: false
      become: true

    - name: Enable and start podman socket (user scope)
      ansible.builtin.systemd:
        name: podman.socket
        scope: user
        enabled: true
        state: started

    # ------------------------------------------------------------------
    # 2. Create host directory structure for config and CA trust
    #    Only the config/cert dirs and the CA trust dir are bind-mounted.
    #    Runtime dirs (run, data, home, runner) use named podman volumes.
    # ------------------------------------------------------------------
    - name: Create AAP directory structure
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        mode: '0750'
      loop:
        - "{{ aap_base_dir }}"
        - "{{ ca_tls_dir }}"
        - "{{ receptor_conf_dir }}"
        - "{{ aap_base_dir }}/containers"
        - "{{ ansible_user_dir }}/.config/systemd/user"

    # ------------------------------------------------------------------
    # 3. Extract CA trust bundle
    #    Restores ~/aap/tls/extracted/ which the receptor container mounts
    #    at /etc/pki/ca-trust/extracted — this is how it trusts the mesh CA.
    # ------------------------------------------------------------------
    - name: Extract CA trust bundle
      ansible.builtin.command:
        cmd: >-
          tar -xzf {{ bundle_dir | default(playbook_dir) }}/ca-trust-extracted.tar.gz
          -C {{ ca_tls_dir }}
      args:
        creates: "{{ ca_tls_dir }}/extracted"
      changed_when: true

    # ------------------------------------------------------------------
    # 4. Deploy receptor config and TLS certificates
    #    All files land in ~/aap/receptor/etc/ (receptor_conf_dir).
    #    This mirrors where the installer places them on the controller:
    #      receptor.conf          → bind-mounted as /etc/receptor/receptor.conf:ro
    #      receptor.crt           → bind-mounted as /etc/receptor/tls/receptor.crt:ro
    #      receptor.key           → bind-mounted as /etc/receptor/tls/receptor.key:ro
    #      mesh-CA.crt            → bind-mounted as /etc/receptor/tls/ca/mesh-CA.crt:ro
    #      signing_public.pem     → bind-mounted as /etc/receptor/signing_public.pem:ro
    # ------------------------------------------------------------------
    - name: Deploy receptor.conf
      ansible.builtin.copy:
        src: "{{ bundle_dir | default(playbook_dir) }}/receptor.conf"
        dest: "{{ receptor_conf_dir }}/receptor.conf"
        mode: '0644'
      notify: Restart receptor

    - name: Deploy receptor private key
      ansible.builtin.copy:
        src: "{{ bundle_dir | default(playbook_dir) }}/certs/receptor.key"
        dest: "{{ receptor_conf_dir }}/receptor.key"
        mode: '0400'
      notify: Restart receptor

    - name: Deploy receptor certificate
      ansible.builtin.copy:
        src: "{{ bundle_dir | default(playbook_dir) }}/certs/receptor.crt"
        dest: "{{ receptor_conf_dir }}/receptor.crt"
        mode: '0640'
      notify: Restart receptor

    - name: Deploy mesh CA certificate
      ansible.builtin.copy:
        src: "{{ bundle_dir | default(playbook_dir) }}/certs/mesh-CA.crt"
        dest: "{{ receptor_conf_dir }}/mesh-CA.crt"
        mode: '0640'
      notify: Restart receptor

    - name: Deploy work signing public key
      ansible.builtin.copy:
        src: "{{ bundle_dir | default(playbook_dir) }}/certs/signing_public.pem"
        dest: "{{ receptor_conf_dir }}/signing_public.pem"
        mode: '0644'
      notify: Restart receptor

    # ------------------------------------------------------------------
    # 5. Create named podman volumes for receptor runtime data
    #    Matches roles/receptor/tasks/volumes.yml in the installer.
    #    Using named volumes (not bind mounts) for run/data/home/runner
    #    ensures correct uid mapping with --userns keep-id.
    # ------------------------------------------------------------------
    - name: Create named podman volumes for receptor
      ansible.builtin.command:
        cmd: "podman volume create {{ item }}"
      loop:
        - "{{ receptor_run_volume }}"
        - "{{ receptor_runner_volume }}"
        - "{{ receptor_home_volume }}"
        - "{{ receptor_data_volume }}"
      register: _volume_create
      changed_when: "'already exists' not in _volume_create.stderr"
      failed_when:
        - _volume_create.rc != 0
        - "'already exists' not in _volume_create.stderr"

    # ------------------------------------------------------------------
    # 6. Load container images from bundle
    # ------------------------------------------------------------------
    - name: Find all container images in bundle
      ansible.builtin.find:
        paths: "{{ bundle_dir | default(playbook_dir) }}/images"
        patterns: "*.tar.gz"
      register: _bundle_images

    - name: Load container images into podman
      ansible.builtin.command:
        cmd: "podman load -i {{ item.path }}"
      loop: "{{ _bundle_images.files }}"
      loop_control:
        label: "{{ item.path | basename }}"
      register: _image_load
      changed_when: "'Loaded image' in _image_load.stdout"

    # ------------------------------------------------------------------
    # 7. Detect loaded receptor image name
    #    After podman load, use the exact image name from the load output
    #    or fall back to the inventory-supplied receptor_image variable.
    # ------------------------------------------------------------------
    - name: Get loaded receptor image name
      ansible.builtin.command:
        cmd: >-
          podman images --format "{{ '{{' }}.Repository{{ '}}' }}:{{ '{{' }}.Tag{{ '}}' }}"
          --filter "reference=*receptor*"
      register: _loaded_receptor_image
      changed_when: false

    - name: Set receptor image fact from loaded image
      ansible.builtin.set_fact:
        _receptor_image_to_use: "{{ _loaded_receptor_image.stdout_lines[0] | trim }}"
      when:
        - _loaded_receptor_image.stdout_lines | length > 0
        - _loaded_receptor_image.stdout_lines[0] | trim | length > 0

    - name: Fall back to inventory receptor_image if auto-detect failed
      ansible.builtin.set_fact:
        _receptor_image_to_use: "{{ receptor_image }}"
      when: _receptor_image_to_use is not defined

    # ------------------------------------------------------------------
    # 8. Remove existing receptor container if present
    # ------------------------------------------------------------------
    - name: Check if receptor container already exists
      ansible.builtin.command:
        cmd: "podman container exists {{ receptor_container_name }}"
      register: _container_exists
      changed_when: false
      failed_when: false

    - name: Stop receptor service if running
      ansible.builtin.systemd:
        name: receptor.service
        scope: user
        state: stopped
      when: _container_exists.rc == 0
      failed_when: false

    - name: Remove existing receptor container
      ansible.builtin.command:
        cmd: "podman rm -f {{ receptor_container_name }}"
      when: _container_exists.rc == 0
      changed_when: true

    # ------------------------------------------------------------------
    # 9. Create the receptor container
    #    Volume layout matches roles/receptor/tasks/facts.yml exactly:
    #
    #    Named volumes (with :U for uid/gid remapping):
    #      receptor_run     → /run/receptor
    #      receptor_runner  → /home/runner
    #      receptor_home    → /home/<ansible_user_id>
    #      receptor_data    → /var/lib/receptor
    #
    #    Bind mounts (read-only, :ro,z):
    #      receptor_conf_dir/receptor.conf      → /etc/receptor/receptor.conf
    #      receptor_conf_dir/receptor.crt       → /etc/receptor/tls/receptor.crt
    #      receptor_conf_dir/receptor.key       → /etc/receptor/tls/receptor.key
    #      receptor_conf_dir/mesh-CA.crt        → /etc/receptor/tls/ca/mesh-CA.crt
    #      receptor_conf_dir/signing_public.pem → /etc/receptor/signing_public.pem
    #      ca_tls_dir/extracted                 → /etc/pki/ca-trust/extracted
    #
    #    NOTE: The installer also mounts ~/aap/containers/podman and the
    #    podman socket for EE spawning. On a standalone exec node these are
    #    only needed if you want the node to launch containers itself (e.g.
    #    ansible-runner with EE images). They are included here for full
    #    compatibility with the AAP controller job dispatch.
    # ------------------------------------------------------------------
    - name: Create the receptor container
      ansible.builtin.command:
        cmd: >-
          podman create
          --name {{ receptor_container_name }}
          --user {{ ansible_user_uid }}
          --userns keep-id
          --network host
          --log-driver {{ container_log_driver | default('journald') }}
          --security-opt label=disable
          -v {{ receptor_run_volume }}:/run/receptor:U
          -v {{ receptor_runner_volume }}:/home/runner:U
          -v {{ receptor_home_volume }}:/home/{{ ansible_user_id }}:U
          -v {{ receptor_data_volume }}:/var/lib/receptor:U
          -v {{ receptor_conf_dir }}/receptor.conf:/etc/receptor/receptor.conf:ro,z
          -v {{ receptor_conf_dir }}/receptor.crt:/etc/receptor/tls/receptor.crt:ro,z
          -v {{ receptor_conf_dir }}/receptor.key:/etc/receptor/tls/receptor.key:ro,z
          -v {{ receptor_conf_dir }}/mesh-CA.crt:/etc/receptor/tls/ca/mesh-CA.crt:ro,z
          -v {{ receptor_conf_dir }}/signing_public.pem:/etc/receptor/signing_public.pem:ro,z
          -v {{ ca_tls_dir }}/extracted:/etc/pki/ca-trust/extracted:z
          -v /run/user/{{ ansible_user_uid }}/podman/podman.sock:/run/user/{{ ansible_user_uid }}/podman/podman.sock:z
          -e HOME={{ ansible_user_dir }}
          -e RECEPTORCTL_SOCKET=/run/receptor/receptor.sock
          -e CONTAINER_HOST=unix://run/user/{{ ansible_user_uid }}/podman/podman.sock
          {{ _receptor_image_to_use }}
          /usr/bin/receptor --config /etc/receptor/receptor.conf
      changed_when: true

    # ------------------------------------------------------------------
    # 10. Generate systemd unit and enable receptor service
    #
    #     The containers.podman collection (used by the AAP installer) runs:
    #       podman generate systemd <name> --format json --name \
    #         --container-prefix='' --separator=''
    #     and writes the file itself from the JSON output, producing:
    #       receptor.service
    #
    #     When calling podman generate systemd directly from the shell:
    #     - In Podman 4, --name is a bool flag (use container name, not ID).
    #     - In Podman 5, --name is positional (the container name argument).
    #     - Default --container-prefix=container + --separator=- means
    #       `--name` alone produces 'container-receptor.service', NOT
    #       'receptor.service'.
    #     - --files writes to CWD but names the file after the prefix+name.
    #
    #     The only reliable cross-version approach: pipe stdout directly
    #     into the target service file using shell redirect, with explicit
    #     empty prefix and separator to match what the installer produces.
    # ------------------------------------------------------------------
    - name: Generate systemd user unit for receptor container
      ansible.builtin.shell:
        cmd: >-
          podman generate systemd
          --name
          --container-prefix=''
          --separator=''
          {{ receptor_container_name }}
          > {{ ansible_user_dir }}/.config/systemd/user/receptor.service
      changed_when: true

    - name: Reload systemd user daemon
      ansible.builtin.systemd:
        daemon_reload: true
        scope: user

    - name: Enable and start receptor service
      ansible.builtin.systemd:
        name: receptor.service
        scope: user
        enabled: true
        state: started

    # ------------------------------------------------------------------
    # 11. Open firewall port (if firewalld is running)
    # ------------------------------------------------------------------
    - name: Check if firewalld is running
      ansible.builtin.command:
        cmd: firewall-cmd --state
      register: _firewalld_state
      changed_when: false
      failed_when: false

    - name: Open receptor port in firewall
      ansible.builtin.command:
        cmd: >-
          firewall-cmd --permanent --zone=public
          --add-port={{ receptor_port }}/{{ receptor_protocol }}
      become: true
      when: _firewalld_state.rc == 0
      register: _fw_result
      changed_when: "'success' in _fw_result.stdout"

    - name: Reload firewall
      ansible.builtin.command:
        cmd: firewall-cmd --reload
      become: true
      when:
        - _firewalld_state.rc == 0
        - _fw_result is changed
      changed_when: true

    # ------------------------------------------------------------------
    # 12. Verify receptor is running
    #     receptorctl is NOT present inside the receptor container image.
    #     Instead we check via the socket using the receptor binary itself,
    #     or simply verify the service is active and the socket exists.
    # ------------------------------------------------------------------
    - name: Wait for receptor socket to appear
      ansible.builtin.wait_for:
        path: "{{ ansible_user_dir }}/.local/share/containers/storage/volumes/receptor_run/_data/receptor.sock"
        timeout: 60

    - name: Verify receptor service is active
      ansible.builtin.command:
        cmd: systemctl --user is-active receptor.service
      register: _receptor_service_status
      changed_when: false
      retries: 5
      delay: 5
      until: _receptor_service_status.rc == 0

    - name: Show receptor service status
      ansible.builtin.command:
        cmd: systemctl --user status receptor.service --no-pager
      register: _receptor_status_output
      changed_when: false
      failed_when: false

    - name: Display receptor service status
      ansible.builtin.debug:
        msg: "{{ _receptor_status_output.stdout }}"

    - name: Display installation summary
      ansible.builtin.debug:
        msg: |
          ===============================================================
          Receptor installed successfully
          ===============================================================

          Node     : {{ exec_node_hostname }} ({{ exec_node_type }})
          Port     : {{ receptor_port }}/{{ receptor_protocol }}
          Image    : {{ _receptor_image_to_use }}
          Config   : {{ receptor_conf_dir }}/receptor.conf
          Service  : receptor.service (user scope)

          Useful commands:
            systemctl --user status receptor.service
            systemctl --user restart receptor.service
            podman logs receptor
            journalctl --user -u receptor.service -f

          Verify in AAP UI (on the controller):
            Administration -> Instances -> {{ exec_node_hostname }}
            Administration -> Topology View
          ===============================================================

  handlers:
    - name: Restart receptor
      ansible.builtin.systemd:
        name: receptor.service
        scope: user
        state: restarted
      failed_when: false
