01_constructor.ymlΒΆ

---
- name: πŸ” Detect container environment (systemd-detect-virt)
  ansible.builtin.command: systemd-detect-virt --container
  register: systemd_detect_virt_container
  changed_when: false
  failed_when: false

- name: πŸ“ Normalize DOCKER_IN_CONTAINER from runtime detection
  ansible.builtin.set_fact:
    DOCKER_IN_CONTAINER: "{{ (systemd_detect_virt_container.rc | default(1) | int) == 0 }}"

- name: "πŸ”“ Get apt ready before the first apt task"
  ansible.builtin.include_tasks: "./tasks/utils/apt/ready.yml"
  when: ansible_facts['os_family'] == 'Debian'

- name: πŸ”Ž Check DNS resolution for test domain
  ansible.builtin.command: "getent hosts {{ DOMAIN_TEST }}"
  register: dns_check
  changed_when: false
  failed_when: dns_check.rc != 0
  when:
    - MODE_ASSERT | bool
    - not (DOMAIN_PRIMARY is search('\.onion$'))

- name: πŸ”€ Merge variables
  block:
    - name: πŸ”€ Merge networks definitions
      ansible.builtin.set_fact:
        networks: "{{ defaults_networks | combine(networks | default({}, true), recursive=True) }}"

    - name: 🧊 Render the applications SPOT once for the merge block
      applications_carrier: {}

    - name: "βœ… Sanity check: all group_names must exist in applications"
      ansible.builtin.assert:
        that:
          - group_name in lookup('applications')
        fail_msg: "Group '{{ group_name }}' has no entry in 'applications'"
        success_msg: "Group '{{ group_name }}' is defined in 'applications'"
      loop: "{{ group_names }}"
      loop_control:
        loop_var: group_name
      when: MODE_ASSERT | bool

    - name: πŸ”€ Merge current play applications
      ansible.builtin.set_fact:
        CURRENT_PLAY_APPLICATIONS: "{{ lookup('applications_current_play') }}"
      no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"

    - name: πŸ”€ Merge current play domain definitions
      ansible.builtin.set_fact:
        CURRENT_PLAY_DOMAINS: >-
          {{
            lookup('applications')
            | canonical_domains_map(
                DOMAIN_PRIMARY,
                recursive=True,
                roles_base_dir=(lookup('path_absolute', 'roles')),
                seed=(CURRENT_PLAY_APPLICATIONS | dict2items | map(attribute='key') | list)
              )
          }}

    - name: πŸ”§ Set CURRENT_PLAY_REDIRECT_DOMAINS
      ansible.builtin.set_fact:
        CURRENT_PLAY_REDIRECT_DOMAINS: "{{ lookup('current_play_redirect_domains') }}"

    - name: πŸ”§ Set current play all domains incl. www redirect if enabled
      ansible.builtin.set_fact:
        CURRENT_PLAY_DOMAINS_ALL: >-
          {{
            (
              CURRENT_PLAY_DOMAINS
              | combine(
                  CURRENT_PLAY_REDIRECT_DOMAINS | default([])
                  | items2dict(key_name='source', value_name='source'),
                  recursive=True
                )
            )
            | generate_all_domains(WWW_REDIRECT_ENABLED | bool)
          }}

    - name: βœ… Assert OIDC resolves lazily (inventory overrides live in OIDC_OVERRIDE)
      ansible.builtin.assert:
        that:
          - OIDC.CLIENT.DISCOVERY_DOCUMENT is defined
        fail_msg: >-
          OIDC does not resolve to the full defaults tree. An inventory most
          likely still defines 'OIDC:' at host level, shadowing the lazy
          group_var β€” migrate that block to 'OIDC_OVERRIDE:' so the issuer can
          family-align to each consuming role.
        quiet: true
      no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"

    - name: πŸ”€ Merge design configuration
      ansible.builtin.set_fact:
        design: "{{ defaults_design | combine(design | default({}, true), recursive=True) }}"

    - name: πŸ”€ Merge service_provider configuration
      ansible.builtin.set_fact:
        service_provider: "{{ defaults_service_provider | combine(service_provider | default({}, true), recursive=True) }}"

    - name: 🧊 Drop the merge-block carrier so later stages re-render
      applications_carrier:
        clear: true

- name: "πŸ’Ύ Persist {{ SOFTWARE_NAME }} version"
  ansible.builtin.include_role:
    name: sys-version

- name: 🎬 init root user
  ansible.builtin.include_role:
    name: user-root

- name: "πŸ₯Ύ Bootstrap svc-net-wireguard"
  when: "'svc-net-wireguard' | application_allowed(group_names, lookup('deployment').whitelist)"
  block:
    - name: 🧊 Carry the applications SPOT through the mesh bootstrap
      applications_carrier: {}

    - name: "πŸ”Œ Load svc-net-wireguard"
      ansible.builtin.include_role:
        name: svc-net-wireguard

    - name: 🧊 Drop the mesh-bootstrap carrier so later stages re-render
      applications_carrier:
        clear: true

- name: πŸ”„ update device
  ansible.builtin.include_role:
    name: update

- name: "βœ… Ensure correct timezone is '{{ HOST_TIMEZONE }}'"
  community.general.timezone:
    name: "{{ HOST_TIMEZONE }}"
  when: not DOCKER_IN_CONTAINER | bool

- name: 🧹 System Cleanup
  ansible.builtin.include_role:
    name: sys-cleanup
  when:
    - MODE_CLEANUP | bool
    - DOCKER_IN_CONTAINER | bool

- name: "πŸ–₯️ Auto-place single-host shared services on first manager (swarm mode)"
  ansible.builtin.add_host:
    name: "{{ (groups['svc-swarm-manager'] | default([]))[0] }}"
    groups: "{{ role_id }}"
  loop: "{{ lookup('roles_with_placement', 'manager') }}"
  loop_control:
    loop_var: role_id
    label: "{{ role_id }}"
  when:
    - (groups['svc-swarm-manager'] | default([]) | length) > 0
    - role_id in groups
    - (groups[role_id] | default([]) | length) == 0
  run_once: true
  changed_when: false

- name: "πŸ”§ auto-place svc-prx-openresty (swarm manager, first compose host) for vhosts"
  ansible.builtin.add_host:
    name: "{{ (groups['svc-swarm-manager'] | default([]))[0] | default(ansible_play_hosts | first) }}"
    groups: svc-prx-openresty
  vars:
    _web_groups: >-
      {{
        groups
        | dict2items
        | selectattr('key', 'match', '^web-')
        | rejectattr('value', 'equalto', [])
        | list
      }}
  when:
    - (groups['svc-prx-openresty'] | default([]) | length) == 0
    - (_web_groups | length) > 0
  run_once: true
  changed_when: false

- name: 🧊 Render the applications SPOT once for the bootstrap section
  applications_carrier: {}
  when: DEPLOYMENT_MODE == 'swarm'

- name: "πŸ₯Ύ Bootstrap svc-swarm-node"
  ansible.builtin.include_role:
    name: svc-swarm-node
  when: "'svc-swarm-node' | application_allowed(group_names, lookup('deployment').whitelist)"

- name: "πŸ“‚ Open service-loader preload context BEFORE Bootstrap"
  ansible.builtin.set_fact:
    service_loader_post_run: []
    service_loader_preloading: true
  changed_when: false

- name: "πŸ₯Ύ Bootstrap NFS clients + registry + sandbox runtimes (MUST precede Preload)"
  ansible.builtin.include_tasks: ./tasks/utils/setup/group.yml
  loop:
    - svc-storage
    - svc-registry
    - svc-virt
  loop_control:
    loop_var: role_group
    label: "{{ role_group }}"

- name: πŸ“‚ Include Token Store
  ansible.builtin.include_role:
    name: sys-token-store
  when: run_once_sys_token_store is not defined

- name: 🧊 Render the applications SPOT once and carry it into every task's worker
  applications_carrier: {}

- name: πŸ› οΈ Preload shared services
  ansible.builtin.include_role:
    name: sys-service-loader

- name: "πŸ“‚ Close the service-loader preload context"
  ansible.builtin.set_fact:
    service_loader_preloading: false
  changed_when: false

- name: "🧩 Load base roles"
  ansible.builtin.include_tasks: ./tasks/utils/setup/group.yml
  loop: "{{ lookup('stage_groups', 'constructor') }}"
  loop_control:
    loop_var: role_group
    label: "{{ role_group }}"

- name: πŸ“¦ Install inventory-defined packages
  package_install:
    id: "{{ PACKAGES }}"
  when: PACKAGES | default([]) | length > 0