346 lines
12 KiB
YAML
346 lines
12 KiB
YAML
---
|
|
# showcase/k3s-worker.yml — K3s agent (worker node) setup
|
|
#
|
|
# Discovers the control plane instance via Incus API, reads the join
|
|
# token from the control instance, and installs K3s agent joining
|
|
# the discovered control plane.
|
|
#
|
|
# Note: The Aether blueprint profile should include:
|
|
# security.nesting: "true"
|
|
|
|
# --- Step 1: Find the control plane from the deployment manifest ---
|
|
|
|
- name: Find control plane instances from deployment
|
|
ansible.builtin.set_fact:
|
|
control_instances: >-
|
|
{{ ffsdn_instances | selectattr('component_name', 'equalto', 'control') | list }}
|
|
|
|
- name: Validate control plane found
|
|
ansible.builtin.assert:
|
|
that:
|
|
- control_instances | length > 0
|
|
fail_msg: >-
|
|
No control component found in ffsdn_instances. The blueprint must
|
|
include a control component for worker nodes to join.
|
|
|
|
- name: Select control plane instance
|
|
ansible.builtin.set_fact:
|
|
control_instance:
|
|
name: "{{ control_instances[0].name }}"
|
|
ip: "{{ control_instances[0].ip }}"
|
|
|
|
- name: Display control plane target
|
|
ansible.builtin.debug:
|
|
msg: "Joining control plane: {{ control_instance.name }} at {{ control_instance.ip }}"
|
|
|
|
# --- Step 2: Wait for and read the join token ---
|
|
|
|
- name: Wait for K3s join token to be available (up to 10 minutes)
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/instances/{{ control_instance.name }}/files?path=/var/lib/rancher/k3s/server/token"
|
|
method: GET
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
return_content: true
|
|
status_code: [200, 404]
|
|
register: k3s_token_response
|
|
until: k3s_token_response.status == 200
|
|
retries: 60
|
|
delay: 10
|
|
|
|
- name: Extract join token
|
|
ansible.builtin.set_fact:
|
|
k3s_server_url: "https://{{ control_instance.ip }}:6443"
|
|
k3s_token: "{{ k3s_token_response.content | trim }}"
|
|
|
|
# --- Step 2b: Add ACL rules for K3s communication ---
|
|
|
|
- name: Build control ACL name
|
|
ansible.builtin.set_fact:
|
|
control_acl_name: "52-{{ control_instance.name }}-aether-acl"
|
|
|
|
- name: Read current control ACL
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/network-acls/{{ control_acl_name }}"
|
|
method: GET
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
return_content: true
|
|
status_code: [200, 404]
|
|
register: control_acl_response
|
|
|
|
- name: Build updated control ingress rules (allow worker to reach K3s API)
|
|
ansible.builtin.set_fact:
|
|
updated_control_ingress: >-
|
|
{{ (control_acl_response.json.metadata.ingress | default([])) + [
|
|
{
|
|
'action': 'allow',
|
|
'state': 'enabled',
|
|
'source': (ffsdn_instance_ip | default('')) + '/32',
|
|
'destination': control_instance.ip + '/32',
|
|
'protocol': 'tcp',
|
|
'destination_port': '6443,10250',
|
|
'description': 'Allow ' + ffsdn_instance_name + ' to control K3s TCP ports'
|
|
},
|
|
{
|
|
'action': 'allow',
|
|
'state': 'enabled',
|
|
'source': (ffsdn_instance_ip | default('')) + '/32',
|
|
'destination': control_instance.ip + '/32',
|
|
'protocol': 'udp',
|
|
'destination_port': '8472',
|
|
'description': 'Allow ' + ffsdn_instance_name + ' to control VXLAN'
|
|
}
|
|
] }}
|
|
when: control_acl_response.status == 200
|
|
|
|
- name: Update control ACL with worker access rule
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/network-acls/{{ control_acl_name }}"
|
|
method: PATCH
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
body_format: json
|
|
body:
|
|
ingress: "{{ updated_control_ingress }}"
|
|
status_code: [200]
|
|
when:
|
|
- control_acl_response.status == 200
|
|
- updated_control_ingress is defined
|
|
|
|
- name: Build worker ACL name
|
|
ansible.builtin.set_fact:
|
|
worker_acl_name: "52-{{ ffsdn_instance_name }}-aether-acl"
|
|
|
|
- name: Read current worker ACL
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/network-acls/{{ worker_acl_name }}"
|
|
method: GET
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
return_content: true
|
|
status_code: [200, 404]
|
|
register: worker_acl_response
|
|
|
|
- name: Build updated worker ingress rules (allow control to reach kubelet)
|
|
ansible.builtin.set_fact:
|
|
updated_worker_ingress: >-
|
|
{{ (worker_acl_response.json.metadata.ingress | default([])) + [
|
|
{
|
|
'action': 'allow',
|
|
'state': 'enabled',
|
|
'source': control_instance.ip + '/32',
|
|
'destination': (ffsdn_instance_ip | default('')) + '/32',
|
|
'protocol': 'tcp',
|
|
'destination_port': '10250',
|
|
'description': 'Allow control to reach ' + ffsdn_instance_name + ' kubelet'
|
|
},
|
|
{
|
|
'action': 'allow',
|
|
'state': 'enabled',
|
|
'source': control_instance.ip + '/32',
|
|
'destination': (ffsdn_instance_ip | default('')) + '/32',
|
|
'protocol': 'udp',
|
|
'destination_port': '8472',
|
|
'description': 'Allow control VXLAN to ' + ffsdn_instance_name
|
|
}
|
|
] }}
|
|
when: worker_acl_response.status == 200
|
|
|
|
- name: Update worker ACL with control access rule
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/network-acls/{{ worker_acl_name }}"
|
|
method: PATCH
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
body_format: json
|
|
body:
|
|
ingress: "{{ updated_worker_ingress }}"
|
|
status_code: [200]
|
|
when:
|
|
- worker_acl_response.status == 200
|
|
- updated_worker_ingress is defined
|
|
|
|
- name: Report ACL updates
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
ACL rules added: {{ ffsdn_instance_name }} ↔ {{ control_instance.name }}
|
|
(K3s API 6443, kubelet 10250, VXLAN 8472)
|
|
|
|
# --- Step 3: Install K3s agent ---
|
|
|
|
- name: Generate K3s agent setup script
|
|
ansible.builtin.set_fact:
|
|
k3s_agent_script: |
|
|
#!/bin/bash
|
|
exec > >(tee /tmp/k3s-agent-setup.log) 2>&1
|
|
set -e
|
|
|
|
echo "=== K3s agent setup starting ==="
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
# Container workaround: create /dev/kmsg if missing
|
|
[ -e /dev/kmsg ] || ln -s /dev/console /dev/kmsg
|
|
|
|
# Install prerequisites
|
|
apt-get install -y -qq curl iptables 2>&1 | tail -1
|
|
echo "[OK] curl + iptables installed"
|
|
|
|
# Test connectivity to control plane before installing
|
|
echo "Testing connectivity to {{ k3s_server_url }}..."
|
|
if curl -sk --connect-timeout 5 "{{ k3s_server_url }}" >/dev/null 2>&1; then
|
|
echo "[OK] Control plane reachable"
|
|
else
|
|
echo "[WARN] Control plane not reachable via curl (may still work via K3s)"
|
|
fi
|
|
|
|
# Download and install K3s binary (skip service start)
|
|
echo "Downloading K3s install script..."
|
|
curl -sfL https://get.k3s.io -o /tmp/k3s-install.sh
|
|
chmod +x /tmp/k3s-install.sh
|
|
echo "[OK] Install script downloaded"
|
|
|
|
echo "Installing K3s agent binary (skip start)..."
|
|
K3S_URL="{{ k3s_server_url }}" \
|
|
K3S_TOKEN="{{ k3s_token }}" \
|
|
INSTALL_K3S_EXEC="--snapshotter=native --kubelet-arg=feature-gates=KubeletInUserNamespace=true --node-ip={{ ffsdn_instance_ip | default('127.0.0.1') }}" \
|
|
INSTALL_K3S_SKIP_START=true \
|
|
/tmp/k3s-install.sh
|
|
echo "[OK] K3s agent binary installed"
|
|
|
|
# Show resource limits
|
|
echo "--- container limits ---"
|
|
echo "Memory limit: $(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo unknown)"
|
|
echo "PIDs max: $(cat /sys/fs/cgroup/pids.max 2>/dev/null || echo unknown)"
|
|
free -m 2>/dev/null || true
|
|
|
|
# Write config file (K3s re-execs itself and loses CLI args)
|
|
mkdir -p /etc/rancher/k3s
|
|
cat > /etc/rancher/k3s/config.yaml <<'CFGEOF'
|
|
server: "{{ k3s_server_url }}"
|
|
token: "{{ k3s_token }}"
|
|
snapshotter: native
|
|
kubelet-arg:
|
|
- "feature-gates=KubeletInUserNamespace=true"
|
|
node-ip: "{{ ffsdn_instance_ip | default('127.0.0.1') }}"
|
|
CFGEOF
|
|
echo "[OK] K3s agent config written to /etc/rancher/k3s/config.yaml"
|
|
|
|
# Start K3s agent directly (bypass systemd/D-Bus to avoid container crash)
|
|
echo "Starting K3s agent directly..."
|
|
nohup /usr/local/bin/k3s agent \
|
|
> /var/log/k3s-agent.log 2>&1 &
|
|
K3S_PID=$!
|
|
echo "[OK] K3s agent started (PID $K3S_PID)"
|
|
|
|
# Wait for agent to initialize (check process is still alive)
|
|
echo "Waiting for K3s agent to initialize..."
|
|
for i in $(seq 1 12); do
|
|
sleep 5
|
|
if ! kill -0 $K3S_PID 2>/dev/null; then
|
|
echo "[ERROR] K3s agent process died"
|
|
echo "--- last 30 lines of agent log ---"
|
|
tail -30 /var/log/k3s-agent.log 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
echo " ... alive after ${i}x5s (PID $K3S_PID)"
|
|
done
|
|
echo "[OK] K3s agent running for 60s"
|
|
|
|
# Verify node is registered with the cluster
|
|
echo "--- K3s agent log (last 10 lines) ---"
|
|
tail -10 /var/log/k3s-agent.log 2>/dev/null || true
|
|
|
|
echo "=== K3s agent setup complete ==="
|
|
|
|
- name: Push K3s agent setup script
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/k3s-agent-setup.sh"
|
|
method: POST
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
body: "{{ k3s_agent_script }}"
|
|
headers:
|
|
Content-Type: "application/octet-stream"
|
|
X-Incus-type: "file"
|
|
X-Incus-mode: "0755"
|
|
status_code: [200]
|
|
|
|
- name: Execute K3s agent setup script
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/exec"
|
|
method: POST
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
body_format: json
|
|
body:
|
|
command: ["/bin/bash", "/tmp/k3s-agent-setup.sh"]
|
|
record-output: true
|
|
interactive: false
|
|
wait-for-websocket: false
|
|
environment:
|
|
DEBIAN_FRONTEND: "noninteractive"
|
|
status_code: [202]
|
|
return_content: true
|
|
register: k3s_agent_exec
|
|
|
|
- name: Wait for K3s agent setup to complete (up to 10 minutes)
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}{{ k3s_agent_exec.json.operation }}/wait?timeout=600"
|
|
method: GET
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
return_content: true
|
|
timeout: 620
|
|
register: k3s_agent_wait
|
|
failed_when: false
|
|
|
|
- name: Retrieve K3s agent setup log from container
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/files?path=/tmp/k3s-agent-setup.log"
|
|
method: GET
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
return_content: true
|
|
status_code: [200, 404]
|
|
register: k3s_agent_log
|
|
ignore_errors: true
|
|
|
|
- name: Display K3s agent setup log
|
|
ansible.builtin.debug:
|
|
msg: "{{ k3s_agent_log.content | default('No log captured') }}"
|
|
when: k3s_agent_log is defined and k3s_agent_log.status == 200
|
|
ignore_errors: true
|
|
|
|
- name: Verify K3s agent setup exit code
|
|
ansible.builtin.assert:
|
|
that:
|
|
- k3s_agent_wait.json.metadata.metadata.return | int == 0
|
|
fail_msg: "K3s agent setup exited with code {{ k3s_agent_wait.json.metadata.metadata.return }}"
|
|
success_msg: "K3s agent joined control plane at {{ k3s_server_url }}"
|
|
|
|
- name: Clean up setup script
|
|
ansible.builtin.uri:
|
|
url: "{{ incus_api }}/1.0/instances/{{ ffsdn_instance_name }}/exec"
|
|
method: POST
|
|
client_cert: "{{ incus_cert }}"
|
|
client_key: "{{ incus_key }}"
|
|
validate_certs: false
|
|
body_format: json
|
|
body:
|
|
command: ["/bin/rm", "-f", "/tmp/k3s-agent-setup.sh"]
|
|
record-output: false
|
|
interactive: false
|
|
wait-for-websocket: false
|
|
status_code: [202]
|
|
ignore_errors: true
|