--- - name: Skip k3s configuration when disabled ansible.builtin.meta: end_host when: not (k3s_enabled | default(true) | bool) - name: Skip k3s configuration when state is absent ansible.builtin.meta: end_host when: k3s_state == "absent" - name: Set k3s service name ansible.builtin.set_fact: k3s_service_name: "{{ 'k3s' if k3s_role == 'server' else 'k3s-agent' }}" - name: Check if controller has conf/k3s.cfg (optional) ansible.builtin.stat: path: "{{ role_path }}/../../../conf/k3s.cfg" register: _k3s_prole_cfg_stat delegate_to: localhost - name: Load service namespace from k3s.cfg (controller) ansible.builtin.set_fact: k3s_prole_service_namespace_cfg: >- {{ lookup( 'ansible.builtin.ini', 'SERVICE_NAMESPACE section=Global file=' ~ (role_path ~ '/../../../conf/k3s.cfg'), default='', errors='ignore' ) if _k3s_prole_cfg_stat.stat.exists else '' }} changed_when: false - name: Load service hostname from k3s.cfg (controller) ansible.builtin.set_fact: k3s_prole_service_hostname_user_cfg: >- {{ lookup( 'ansible.builtin.ini', 'SERVICE_HOSTNAME section=User file=' ~ (role_path ~ '/../../../conf/k3s.cfg'), default='', errors='ignore' ) if _k3s_prole_cfg_stat.stat.exists else '' }} k3s_prole_service_hostname_global_cfg: >- {{ lookup( 'ansible.builtin.ini', 'SERVICE_HOSTNAME section=Global file=' ~ (role_path ~ '/../../../conf/k3s.cfg'), default='', errors='ignore' ) if _k3s_prole_cfg_stat.stat.exists else '' }} changed_when: false - name: Normalize service hostname variable ansible.builtin.set_fact: k3s_service_hostname: >- {{ (k3s_prole_service_hostname_user_cfg | default('') | trim) if ((k3s_prole_service_hostname_user_cfg | default('') | trim) | length > 0 and (k3s_prole_service_hostname_user_cfg | trim) != '${SERVICE_HOSTNAME}') else (k3s_prole_service_hostname_global_cfg | default('') | trim) if ((k3s_prole_service_hostname_global_cfg | default('') | trim) | length > 0 and (k3s_prole_service_hostname_global_cfg | trim) != '${SERVICE_HOSTNAME}') else (k3s_service_hostname | default('svc.prole.org')) }} changed_when: false - name: Resolve Kong namespaces from k3s.cfg (avoid implicit default namespace) ansible.builtin.set_fact: _k3s_service_namespace_resolved: >- {{ (k3s_prole_service_namespace_cfg | default('')) if (k3s_prole_service_namespace_cfg | default('') | length > 0 and k3s_prole_service_namespace_cfg != '${SERVICE_NAMESPACE}') else 'default' }} changed_when: false - name: Normalize Kong namespace variables ansible.builtin.set_fact: k3s_kong_namespace: >- {{ (k3s_kong_namespace | default('')) if (k3s_kong_namespace | default('') | length > 0 and k3s_kong_namespace != '${SERVICE_NAMESPACE}') else _k3s_service_namespace_resolved }} changed_when: false - name: Normalize svc-check Kong namespace variable ansible.builtin.set_fact: k3s_svc_check_kong_namespace: >- {{ (k3s_svc_check_kong_namespace | default('')) if (k3s_svc_check_kong_namespace | default('') | length > 0 and k3s_svc_check_kong_namespace != '${SERVICE_NAMESPACE}') else k3s_kong_namespace }} changed_when: false - name: Validate resolved Kong namespace variables ansible.builtin.assert: that: - k3s_kong_namespace | length > 0 - k3s_svc_check_kong_namespace | length > 0 fail_msg: >- Resolved Kong namespace variables are empty. Ensure `SERVICE_NAMESPACE` is set in `conf/k3s.cfg` or explicitly set `k3s_kong_namespace` / `k3s_svc_check_kong_namespace` in Ansible inventory. - name: Pre-stage critical images (registry:2, kong, etc.) ansible.builtin.include_tasks: prestage_images.yml when: - k3s_state == "present" - k3s_prestage_images | bool tags: [images] - name: Create ArgoCD hostPath directories on myrddin.prole.org (/synology/d001) ansible.builtin.file: path: "{{ item }}" state: directory mode: "0777" loop: - /synology/d001/argocd - /synology/d001/argocd/home - /synology/d001/argocd/tmp when: - k3s_state == "present" - inventory_hostname == 'myrddin.prole.org' - name: Create ArgoCD hostPath directories on merlin.prole.org (/synology/d002) ansible.builtin.file: path: "{{ item }}" state: directory mode: "0777" loop: - /synology/d002/argocd - /synology/d002/argocd/data - /synology/d002/argocd/tmp when: - k3s_state == "present" - inventory_hostname == 'merlin.prole.org' - name: Create ArgoCD hostPath directories on pi.prole.org (/synology/d003) ansible.builtin.file: path: "{{ item }}" state: directory mode: "0777" loop: - /synology/d003/argocd - /synology/d003/argocd/gpg-keyring - /synology/d003/argocd/tmp - /synology/d003/argocd/helm-working-dir - /synology/d003/argocd/var-files - /synology/d003/argocd/plugins when: - k3s_state == "present" - inventory_hostname == 'pi.prole.org' - name: Wait for k3s Kubernetes API to become ready ansible.builtin.command: k3s kubectl get --raw='/readyz' register: _k3s_readyz changed_when: false retries: 60 delay: 5 until: _k3s_readyz.rc == 0 when: - k3s_state == "present" - k3s_role == 'server' run_once: true - name: Load optional workloads threshold from prole.cfg (controller) ansible.builtin.set_fact: k3s_optional_workloads_min_nodes_cfg: >- {{ lookup( 'ansible.builtin.ini', 'OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES section=Global file=' ~ (role_path ~ '/../../../conf/k3s.cfg'), default='', errors='ignore' ) if _k3s_prole_cfg_stat.stat.exists else '' }} changed_when: false when: - k3s_state == "present" - k3s_role == 'server' run_once: true - name: Validate optional-workloads policy config is present ansible.builtin.assert: that: - _k3s_prole_cfg_stat.stat.exists - (k3s_optional_workloads_min_nodes_cfg | string | trim | length) > 0 - (k3s_optional_workloads_min_nodes_cfg | int) > 0 fail_msg: >- Missing or invalid optional-workloads policy configuration. Expected [Global] OPTIONAL_WORKLOADS_MIN_READY_SCHEDULABLE_NODES in conf/k3s.cfg (got '{{ k3s_optional_workloads_min_nodes_cfg | default('') }}'). changed_when: false when: - k3s_state == "present" - k3s_role == 'server' run_once: true - name: Query cluster nodes for optional-workloads policy ansible.builtin.command: k3s kubectl get nodes --no-headers register: _k3s_nodes_list changed_when: false failed_when: false when: - k3s_state == "present" - k3s_role == 'server' run_once: true - name: Log optional-workloads policy node query failure (treat as insufficient capacity) ansible.builtin.debug: msg: >- Could not query Kubernetes nodes for optional-workloads policy. Treating as ready_schedulable_nodes=0 so optional workloads will be disabled for this run. rc={{ _k3s_nodes_list.rc | default('n/a') }} stderr={{ _k3s_nodes_list.stderr | default('') | trim }} when: - k3s_state == "present" - k3s_role == 'server' - _k3s_nodes_list is defined - (_k3s_nodes_list.rc | default(1) | int) != 0 run_once: true - name: Evaluate optional-workloads policy (ready + schedulable) ansible.builtin.include_tasks: optional_workloads_policy_eval.yml when: - k3s_state == "present" - k3s_role == 'server' run_once: true - name: Log optional-workloads policy decision ansible.builtin.debug: msg: >- optional_workloads_allowed={{ optional_workloads_allowed }} (ready_schedulable_nodes={{ ready_schedulable_nodes }} min_required={{ optional_workloads_min_ready_schedulable_nodes }}) when: - k3s_state == "present" - k3s_role == 'server' run_once: true - name: Apply merlin-local-iscsi StorageClass (monitoring) ansible.builtin.shell: | k3s kubectl apply --validate=false -f - <<'EOF' apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: merlin-local-iscsi provisioner: kubernetes.io/no-provisioner volumeBindingMode: WaitForFirstConsumer reclaimPolicy: Retain EOF register: _merlin_local_iscsi_apply changed_when: >- (_merlin_local_iscsi_apply.stdout | default('') | lower) is search('configured') or (_merlin_local_iscsi_apply.stdout | default('') | lower) is search('created') failed_when: false when: - k3s_state == "present" - k3s_role == 'server' - optional_workloads_allowed | default(false) | bool run_once: true - name: Log monitoring StorageClass apply failure (non-fatal) ansible.builtin.debug: msg: >- Failed to apply monitoring StorageClass (non-fatal): rc={{ _merlin_local_iscsi_apply.rc | default('n/a') }} stderr={{ _merlin_local_iscsi_apply.stderr | default('') | trim }} when: - k3s_state == "present" - k3s_role == 'server' - optional_workloads_allowed | default(false) | bool - _merlin_local_iscsi_apply is defined - (_merlin_local_iscsi_apply.rc | default(0) | int) != 0 run_once: true - name: Skip monitoring StorageClass when optional workloads are disallowed ansible.builtin.debug: msg: >- Skipping monitoring StorageClass because optional workloads are disallowed: optional_workloads_allowed={{ optional_workloads_allowed | default(false) }} (ready_schedulable_nodes={{ ready_schedulable_nodes | default('unknown') }} min_required={{ optional_workloads_min_ready_schedulable_nodes | default('unknown') }}) when: - k3s_state == "present" - k3s_role == 'server' - not (optional_workloads_allowed | default(false) | bool) run_once: true - name: Fetch kubeconfig to controller for controller-side installs ansible.builtin.include_tasks: fetch_kubeconfig.yml when: - k3s_state == "present" - k3s_role == 'server' run_once: true - name: Ensure CloudNative-PG operator and kubectl plugin ansible.builtin.include_tasks: cnpg.yml when: k3s_state == "present" - name: Ensure cert-manager is installed for ACME ansible.builtin.include_tasks: certmgr.yml when: - k3s_state == "present" - k3s_role == 'server' - name: Configure ACME issuer and certificate for service hostname ansible.builtin.include_tasks: acme_cert.yml when: - k3s_state == "present" - k3s_role == 'server' - name: Ensure Kong is present and refreshed ansible.builtin.include_tasks: kong.yml when: - k3s_state == "present" - k3s_role == 'server'