--- - name: Prepare k3s synchronization metadata hosts: k3s_hosts gather_facts: false run_once: true tasks: - name: Initialize k3s sync source host ansible.builtin.set_fact: k3s_sync_source_host: "" delegate_to: localhost delegate_facts: true - name: Select k3s init server as sync source ansible.builtin.set_fact: k3s_sync_source_host: "{{ item }}" loop: "{{ groups['k3s_hosts'] }}" when: - hostvars[item].k3s_role | default('agent') == 'server' - hostvars[item].k3s_cluster_init | default(false) | bool delegate_to: localhost delegate_facts: true - name: Fallback to first k3s server as sync source ansible.builtin.set_fact: k3s_sync_source_host: "{{ item }}" loop: "{{ groups['k3s_hosts'] }}" when: - (hostvars['localhost'].k3s_sync_source_host | length) == 0 - hostvars[item].k3s_role | default('agent') == 'server' delegate_to: localhost delegate_facts: true - name: Require a k3s server as sync source ansible.builtin.assert: that: - hostvars['localhost'].k3s_sync_source_host | length > 0 fail_msg: >- No k3s sync source found. Ensure at least one host has `k3s_role: server`. (If you have multiple servers and want to prefer the bootstrap server, set `k3s_cluster_init: true` on exactly one server.) - name: Ensure sync source is included in this run ansible.builtin.assert: that: - hostvars['localhost'].k3s_sync_source_host in ansible_play_hosts_all fail_msg: "Sync source {{ hostvars['localhost'].k3s_sync_source_host }} is not in this run. Include it in --limit." - name: Create local temp directory for k3s sync ansible.builtin.command: mktemp -d -p /tmp k3s-sync-XXXXXX register: k3s_sync_tmpdir changed_when: true delegate_to: localhost become: false - name: Ensure k3s sync temp directory is writable ansible.builtin.file: path: "{{ k3s_sync_tmpdir.stdout }}" state: directory mode: "1777" delegate_to: localhost - name: Store k3s sync metadata on controller ansible.builtin.set_fact: k3s_sync_source_host: "{{ hostvars['localhost'].k3s_sync_source_host }}" k3s_sync_tmpdir_path: "{{ k3s_sync_tmpdir.stdout }}" k3s_sync_tls_bundle: "{{ k3s_sync_tmpdir.stdout }}/k3s-tls.tgz" delegate_to: localhost delegate_facts: true - name: Collect k3s token and certs from init server hosts: k3s_hosts become: true gather_facts: false run_once: true vars: k3s_sync_source_host: "{{ hostvars['localhost'].k3s_sync_source_host | default('') }}" k3s_sync_tls_bundle: "{{ hostvars['localhost'].k3s_sync_tls_bundle | default('') }}" tasks: - name: Require a k3s sync source ansible.builtin.assert: that: - k3s_sync_source_host | length > 0 fail_msg: "k3s sync source is empty. Ensure the init server is reachable." - name: Ensure k3s node token exists ansible.builtin.stat: path: "{{ k3s_data_dir }}/server/node-token" register: k3s_node_token delegate_to: "{{ k3s_sync_source_host }}" - name: Fail when k3s node token is missing ansible.builtin.fail: msg: "k3s node token not found at {{ k3s_data_dir }}/server/node-token" when: not k3s_node_token.stat.exists - name: Read k3s node token ansible.builtin.slurp: src: "{{ k3s_data_dir }}/server/node-token" register: k3s_node_token_raw delegate_to: "{{ k3s_sync_source_host }}" - name: Store k3s sync token on controller ansible.builtin.set_fact: k3s_sync_token: "{{ k3s_node_token_raw.content | b64decode | trim }}" delegate_to: localhost delegate_facts: true - name: Check for k3s tls directory ansible.builtin.stat: path: "{{ k3s_data_dir }}/server/tls" register: k3s_tls_dir delegate_to: "{{ k3s_sync_source_host }}" - name: Create k3s tls bundle ansible.builtin.archive: path: "{{ k3s_data_dir }}/server/tls" dest: /tmp/k3s-tls.tgz format: gz when: k3s_tls_dir.stat.exists delegate_to: "{{ k3s_sync_source_host }}" - name: Fetch k3s tls bundle ansible.builtin.fetch: src: /tmp/k3s-tls.tgz dest: "{{ k3s_sync_tls_bundle }}" flat: true when: k3s_tls_dir.stat.exists delegate_to: "{{ k3s_sync_source_host }}" - name: Mark tls bundle presence on controller ansible.builtin.set_fact: k3s_sync_tls_bundle_present: "{{ k3s_tls_dir.stat.exists }}" delegate_to: localhost delegate_facts: true - name: Remove temporary tls bundle from source ansible.builtin.file: path: /tmp/k3s-tls.tgz state: absent when: k3s_tls_dir.stat.exists delegate_to: "{{ k3s_sync_source_host }}" - name: Synchronize k3s token and certs to servers hosts: k3s_hosts become: true serial: 1 gather_facts: false vars: k3s_sync_source_host: "{{ hostvars['localhost'].k3s_sync_source_host }}" k3s_sync_token: "{{ hostvars['localhost'].k3s_sync_token | default('') }}" k3s_sync_tls_bundle: "{{ hostvars['localhost'].k3s_sync_tls_bundle | default('') }}" k3s_sync_tls_bundle_present: "{{ hostvars['localhost'].k3s_sync_tls_bundle_present | default(false) }}" pre_tasks: - name: Require k3s sync token ansible.builtin.assert: that: - k3s_sync_token | length > 0 fail_msg: "k3s sync token is empty. Check the init server token." tasks: - name: Stop k3s before syncing ansible.builtin.import_role: name: k3s tasks_from: stop when: inventory_hostname != k3s_sync_source_host - name: Sync k3s token and certs ansible.builtin.import_role: name: k3s tasks_from: sync when: inventory_hostname != k3s_sync_source_host - name: Start k3s after syncing ansible.builtin.import_role: name: k3s tasks_from: start when: inventory_hostname != k3s_sync_source_host - name: Update k3s vault token on controller hosts: k3s_hosts gather_facts: false run_once: true vars: k3s_sync_token: "{{ hostvars['localhost'].k3s_sync_token | default('') }}" vault_k3s_path: "{{ playbook_dir }}/../inventory/group_vars/all/vault_k3s.yml" vault_pass_default: "{{ playbook_dir }}/../../.vault_pass" tasks: - name: Skip vault update when disabled ansible.builtin.meta: end_play when: not (k3s_sync_update_vault | default(true) | bool) - name: Require k3s sync token for vault update ansible.builtin.assert: that: - k3s_sync_token | length > 0 fail_msg: "k3s sync token is empty. Unable to update vault." - name: Check for default vault password file ansible.builtin.stat: path: "{{ vault_pass_default }}" register: vault_pass_default_stat delegate_to: localhost - name: Determine vault password file ansible.builtin.set_fact: k3s_vault_password_file: >- {{ k3s_vault_password_file | default(lookup('env', 'ANSIBLE_VAULT_PASSWORD_FILE') | default('', true), true) }} delegate_to: localhost delegate_facts: true - name: Fallback to default vault password file ansible.builtin.set_fact: k3s_vault_password_file: "{{ vault_pass_default }}" when: - (hostvars['localhost'].k3s_vault_password_file | default('')) | length == 0 - vault_pass_default_stat.stat.exists delegate_to: localhost delegate_facts: true - name: Require vault password file ansible.builtin.assert: that: - (hostvars['localhost'].k3s_vault_password_file | default('')) | length > 0 fail_msg: "Set k3s_vault_password_file or ANSIBLE_VAULT_PASSWORD_FILE to update vault." - name: Ensure vault file exists ansible.builtin.stat: path: "{{ vault_k3s_path }}" register: vault_k3s_file delegate_to: localhost - name: Fail when vault file is missing ansible.builtin.fail: msg: "Vault file not found at {{ vault_k3s_path }}" when: not vault_k3s_file.stat.exists - name: Check if vault file is encrypted ansible.builtin.command: "head -n 1 {{ vault_k3s_path }}" register: vault_k3s_head changed_when: false delegate_to: localhost - name: Mark vault encryption state ansible.builtin.set_fact: vault_k3s_encrypted: "{{ (vault_k3s_head.stdout | default('')) is search('^\\$ANSIBLE_VAULT') }}" delegate_to: localhost delegate_facts: true - name: Decrypt vault_k3s.yml ansible.builtin.command: >- ansible-vault decrypt {{ vault_k3s_path }} --vault-password-file {{ hostvars['localhost'].k3s_vault_password_file }} changed_when: true delegate_to: localhost when: hostvars['localhost'].vault_k3s_encrypted | default(false) - name: Update vault k3s token ansible.builtin.lineinfile: path: "{{ vault_k3s_path }}" regexp: '^vault_k3s_token:' line: "vault_k3s_token: \"{{ k3s_sync_token }}\"" delegate_to: localhost - name: Encrypt vault_k3s.yml ansible.builtin.command: >- ansible-vault encrypt {{ vault_k3s_path }} --vault-password-file {{ hostvars['localhost'].k3s_vault_password_file }} changed_when: true delegate_to: localhost when: hostvars['localhost'].vault_k3s_encrypted | default(false) - name: Refresh kubeconfig on controller hosts: k3s_hosts gather_facts: false run_once: true tasks: - name: Fetch and save kubeconfig on controller ansible.builtin.import_role: name: k3s tasks_from: fetch_kubeconfig - name: Validate k3s nodes from controller hosts: k3s_hosts gather_facts: false run_once: true vars: k3s_kubeconfig_service_path: "{{ playbook_dir }}/../../etc/secrets/k3s.kubeconfig" k3s_kubeconfig_project_path: "{{ playbook_dir }}/../../prole-k3s.kubeconfig" k3s_expected_node_count: "{{ groups['k3s_hosts'] | length }}" tasks: - name: Check for service kubeconfig ansible.builtin.stat: path: "{{ k3s_kubeconfig_service_path }}" register: _k3s_kubeconfig_service_stat delegate_to: localhost become: false - name: Check for project kubeconfig ansible.builtin.stat: path: "{{ k3s_kubeconfig_project_path }}" register: _k3s_kubeconfig_project_stat delegate_to: localhost become: false - name: Select kubeconfig path for validation ansible.builtin.set_fact: k3s_kubeconfig_validation_path: >- {{ k3s_kubeconfig_service_path if _k3s_kubeconfig_service_stat.stat.exists else k3s_kubeconfig_project_path }} changed_when: false - name: Require a kubeconfig for validation ansible.builtin.assert: that: - _k3s_kubeconfig_service_stat.stat.exists or _k3s_kubeconfig_project_stat.stat.exists fail_msg: >- No kubeconfig found at {{ k3s_kubeconfig_service_path }} or {{ k3s_kubeconfig_project_path }}. Run `k3s_reset` or `k3s_sync` again and ensure the init server is reachable. - name: Show nodes (kubectl get nodes -o wide) ansible.builtin.command: "kubectl --kubeconfig {{ k3s_kubeconfig_validation_path }} get nodes -o wide" register: _k3s_nodes_wide changed_when: false delegate_to: localhost become: false - name: Debug nodes output ansible.builtin.debug: var: _k3s_nodes_wide.stdout_lines - name: Count nodes ansible.builtin.shell: | set -euo pipefail kubectl --kubeconfig "{{ k3s_kubeconfig_validation_path }}" get nodes --no-headers | wc -l | tr -d ' ' args: executable: /bin/bash register: _k3s_node_count changed_when: false delegate_to: localhost become: false - name: Require expected node count ansible.builtin.assert: that: - (_k3s_node_count.stdout | int) >= (k3s_expected_node_count | int) fail_msg: >- Expected at least {{ k3s_expected_node_count }} nodes, but kubectl reported {{ _k3s_node_count.stdout | default('0') }}. Ensure all `k3s_hosts` are reachable and have rejoined, then re-run `k3s_sync`.