mirror of
https://github.com/techno-tim/k3s-ansible.git
synced 2026-08-09 07:23:19 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf76292169 | |||
| 52c086d638 | |||
| 010551b8d2 |
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regression test for the Cilium Envoy toggle.
|
||||||
|
|
||||||
|
The `cilium_envoy` variable lets users enable or disable the Cilium Envoy
|
||||||
|
proxy. The Install/upgrade Cilium task in
|
||||||
|
roles/k3s_server_post/tasks/cilium.yml passes the value through to Helm as
|
||||||
|
`envoy.enabled`. This test:
|
||||||
|
|
||||||
|
- loads the real "Install Cilium" task and confirms the install/upgrade
|
||||||
|
command actually contains the `envoy.enabled` Helm value,
|
||||||
|
- renders the conditional that computes the Helm value and confirms it
|
||||||
|
produces `true` when cilium_envoy is enabled and `false` when disabled,
|
||||||
|
- confirms the task stays forward/backward compatible (no raw `true` /
|
||||||
|
`false` hardcoded in place of the conditional).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from jinja2 import Environment
|
||||||
|
|
||||||
|
ENVOY_EXPRESSION = '{{ "true" if cilium_envoy else "false" }}'
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root():
|
||||||
|
return subprocess.check_output(
|
||||||
|
["git", "rev-parse", "--show-toplevel"], text=True
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
raise SystemExit("Cilium Envoy toggle test failed: " + message)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_install_command(path):
|
||||||
|
"""Return the command string for the 'Install Cilium' task.
|
||||||
|
|
||||||
|
Walks both top-level tasks and tasks nested inside a `block`/`always`/
|
||||||
|
`rescue` list, since the Cilium deploy steps are grouped under the
|
||||||
|
'Prepare Cilium CLI on first master and deploy CNI' block.
|
||||||
|
"""
|
||||||
|
with open(path, encoding="utf-8") as handle:
|
||||||
|
doc = yaml.safe_load(handle)
|
||||||
|
|
||||||
|
def find_command(tasks):
|
||||||
|
for task in tasks:
|
||||||
|
if not isinstance(task, dict):
|
||||||
|
continue
|
||||||
|
if task.get("name") == "Install Cilium":
|
||||||
|
command = task.get("ansible.builtin.command")
|
||||||
|
if command is None:
|
||||||
|
raise SystemExit(
|
||||||
|
"Cilium Envoy toggle test failed: "
|
||||||
|
"'Install Cilium' task has no ansible.builtin.command"
|
||||||
|
)
|
||||||
|
return command
|
||||||
|
# Recurse into block/always/rescue sub-lists.
|
||||||
|
for key in ("block", "always", "rescue"):
|
||||||
|
nested = task.get(key)
|
||||||
|
if isinstance(nested, list):
|
||||||
|
found = find_command(nested)
|
||||||
|
if found is not None:
|
||||||
|
return found
|
||||||
|
return None
|
||||||
|
|
||||||
|
command = find_command(doc)
|
||||||
|
if command is None:
|
||||||
|
raise SystemExit(
|
||||||
|
"Cilium Envoy toggle test failed: could not find 'Install Cilium' task"
|
||||||
|
)
|
||||||
|
return command
|
||||||
|
|
||||||
|
|
||||||
|
def assert_envoy_in_command(command):
|
||||||
|
if "envoy.enabled" not in command:
|
||||||
|
fail("install command is missing --helm-set envoy.enabled")
|
||||||
|
if ENVOY_EXPRESSION not in command:
|
||||||
|
fail(
|
||||||
|
"install command does not use the cilium_envoy conditional: "
|
||||||
|
"expected {0!r}".format(ENVOY_EXPRESSION)
|
||||||
|
)
|
||||||
|
# The conditional must be a WYSIWYG helm-set value, not a pre-rendered
|
||||||
|
# true/false literal (which would ignore the cilium_envoy variable).
|
||||||
|
if re.search(r"--helm-set envoy\.enabled=true(?:$|\s)", command):
|
||||||
|
fail("install command hardcodes envoy.enabled=true")
|
||||||
|
if re.search(r"--helm-set envoy\.enabled=false(?:$|\s)", command):
|
||||||
|
fail("install command hardcodes envoy.enabled=false")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_render():
|
||||||
|
env = Environment()
|
||||||
|
|
||||||
|
def render_for(value):
|
||||||
|
template = env.from_string(ENVOY_EXPRESSION)
|
||||||
|
return template.render(cilium_envoy=value)
|
||||||
|
|
||||||
|
if render_for(True) != "true":
|
||||||
|
fail("envoy conditional did not render 'true' when enabled")
|
||||||
|
if render_for(False) != "false":
|
||||||
|
fail("envoy conditional did not render 'false' when disabled")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = repo_root()
|
||||||
|
cilium_tasks = os.path.join(
|
||||||
|
root, "roles", "k3s_server_post", "tasks", "cilium.yml"
|
||||||
|
)
|
||||||
|
command = extract_install_command(cilium_tasks)
|
||||||
|
assert_envoy_in_command(command)
|
||||||
|
assert_render()
|
||||||
|
|
||||||
|
print("Cilium Envoy toggle regression test passed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Regression test for the MetalLB L2Advertisement interfaces.
|
||||||
|
|
||||||
|
`metal_lb_interfaces` restricts which network interfaces MetalLB announces
|
||||||
|
load balancer IPs on in layer2 mode. When the list is non-empty, the
|
||||||
|
L2Advertisement in roles/k3s_server_post/templates/metallb.crs.j2 must render
|
||||||
|
a `spec.interfaces` block; when it is empty (the default), no spec is rendered
|
||||||
|
so MetalLB announces on all interfaces.
|
||||||
|
|
||||||
|
This renders the template and asserts both cases plus the BGP path (which must
|
||||||
|
not be affected by the L2 interfaces variable).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root():
|
||||||
|
return subprocess.check_output(
|
||||||
|
["git", "rev-parse", "--show-toplevel"], text=True
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def fail(message):
|
||||||
|
raise SystemExit("MetalLB interfaces test failed: " + message)
|
||||||
|
|
||||||
|
|
||||||
|
def render(env, extra_vars):
|
||||||
|
base_vars = {
|
||||||
|
"metal_lb_mode": "layer2",
|
||||||
|
"metal_lb_ip_range": "192.168.30.80-192.168.30.90",
|
||||||
|
}
|
||||||
|
base_vars.update(extra_vars)
|
||||||
|
template = env.get_template("metallb.crs.j2")
|
||||||
|
return template.render(**base_vars)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = repo_root()
|
||||||
|
template_dir = os.path.join(
|
||||||
|
root, "roles", "k3s_server_post", "templates"
|
||||||
|
)
|
||||||
|
env = Environment(
|
||||||
|
loader=FileSystemLoader(template_dir), undefined=StrictUndefined
|
||||||
|
)
|
||||||
|
|
||||||
|
# Empty list (default): no spec.interfaces in the L2Advertisement.
|
||||||
|
output = render(env, {"metal_lb_interfaces": []})
|
||||||
|
if "spec:\n interfaces:" in output:
|
||||||
|
fail("spec.interfaces rendered with an empty metal_lb_interfaces")
|
||||||
|
if "kind: L2Advertisement" not in output:
|
||||||
|
fail("L2Advertisement missing in layer2 mode")
|
||||||
|
|
||||||
|
# Single interface.
|
||||||
|
output = render(env, {"metal_lb_interfaces": ["eth1"]})
|
||||||
|
if "spec:\n interfaces:\n - eth1" not in output:
|
||||||
|
fail("single interface was not rendered in spec.interfaces")
|
||||||
|
|
||||||
|
# Multiple interfaces.
|
||||||
|
output = render(env, {"metal_lb_interfaces": ["eth1", "eth2"]})
|
||||||
|
if "spec:\n interfaces:\n - eth1\n - eth2" not in output:
|
||||||
|
fail("multiple interfaces were not rendered in spec.interfaces")
|
||||||
|
|
||||||
|
# BGP mode must not emit an L2Advertisement spec at all.
|
||||||
|
output = render(
|
||||||
|
env,
|
||||||
|
{
|
||||||
|
"metal_lb_mode": "bgp",
|
||||||
|
"metal_lb_interfaces": ["eth1"],
|
||||||
|
"metal_lb_bgp_my_asn": "64513",
|
||||||
|
"metal_lb_bgp_peer_asn": "64512",
|
||||||
|
"metal_lb_bgp_peer_address": "192.168.30.1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if "kind: L2Advertisement" in output:
|
||||||
|
fail("L2Advertisement rendered in bgp mode")
|
||||||
|
if "interfaces:" in output:
|
||||||
|
fail("interfaces rendered in bgp mode")
|
||||||
|
|
||||||
|
print("MetalLB interfaces regression test passed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
repo_root="$(git rev-parse --show-toplevel)"
|
||||||
|
site_play="$repo_root/site.yml"
|
||||||
|
|
||||||
|
# #636: verify the "Pre tasks" play asserts that all k3s_cluster hosts have
|
||||||
|
# unique hostnames, so a duplicate-hostname inventory fails fast instead of
|
||||||
|
# silently breaking node registration/joining.
|
||||||
|
grep -Fq -- 'Verify all cluster nodes have unique hostnames' "$site_play" || {
|
||||||
|
printf 'site.yml is missing the unique-hostname preflight check\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# The check must deduplicate the cluster hostname list via the `unique` filter
|
||||||
|
# and compare lengths, i.e. groups['k3s_cluster'] must be referenced.
|
||||||
|
grep -Fq -- "groups['k3s_cluster']" "$site_play" || {
|
||||||
|
printf 'unique-hostname check does not iterate the k3s_cluster group\n' >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! grep -Eq -- 'cluster_hostnames.*\|.*unique|\| unique' "$site_play"; then
|
||||||
|
printf 'unique-hostname check does not deduplicate the hostname list\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Unique hostname preflight regression test passed\n'
|
||||||
@@ -62,6 +62,12 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
files: ^roles/k3s_server/tasks/(main|join_master)\.yml$|^\.github/scripts/test-k3s-server-bootstrap\.sh$
|
files: ^roles/k3s_server/tasks/(main|join_master)\.yml$|^\.github/scripts/test-k3s-server-bootstrap\.sh$
|
||||||
|
- id: unique-hostname-precheck-test
|
||||||
|
name: Unique hostname precheck test
|
||||||
|
entry: .github/scripts/test-unique-hostname-precheck.sh
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
files: ^site\.yml$|^\.github/scripts/test-unique-hostname-precheck\.sh$
|
||||||
- id: cilium-bgp-manifest-test
|
- id: cilium-bgp-manifest-test
|
||||||
name: Cilium BGP manifest test
|
name: Cilium BGP manifest test
|
||||||
entry: python3 .github/scripts/test-cilium-bgp-manifest.py
|
entry: python3 .github/scripts/test-cilium-bgp-manifest.py
|
||||||
@@ -70,6 +76,15 @@ repos:
|
|||||||
- Jinja2>=3.1
|
- Jinja2>=3.1
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
files: ^roles/k3s_server_post/templates/cilium\.crs\.j2$|^\.github/scripts/test-cilium-bgp-manifest\.py$
|
files: ^roles/k3s_server_post/templates/cilium\.crs\.j2$|^\.github/scripts/test-cilium-bgp-manifest\.py$
|
||||||
|
- id: cilium-envoy-toggle-test
|
||||||
|
name: Cilium Envoy toggle test
|
||||||
|
entry: python3 .github/scripts/test-cilium-envoy-toggle.py
|
||||||
|
language: python
|
||||||
|
additional_dependencies:
|
||||||
|
- Jinja2>=3.1
|
||||||
|
- PyYAML
|
||||||
|
pass_filenames: false
|
||||||
|
files: ^roles/k3s_server_post/tasks/cilium\.yml$|^\.github/scripts/test-cilium-envoy-toggle\.py$
|
||||||
- id: kube-vip-manifest-test
|
- id: kube-vip-manifest-test
|
||||||
name: kube-vip manifest test
|
name: kube-vip manifest test
|
||||||
entry: python3 .github/scripts/test-kube-vip-manifest.py
|
entry: python3 .github/scripts/test-kube-vip-manifest.py
|
||||||
@@ -84,6 +99,14 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
files: ^roles/k3s_server/tasks/metallb\.yml$|^\.github/scripts/test-metallb-remote-read\.sh$
|
files: ^roles/k3s_server/tasks/metallb\.yml$|^\.github/scripts/test-metallb-remote-read\.sh$
|
||||||
|
- id: metallb-interfaces-test
|
||||||
|
name: MetalLB interfaces test
|
||||||
|
entry: python3 .github/scripts/test-metallb-interfaces.py
|
||||||
|
language: python
|
||||||
|
additional_dependencies:
|
||||||
|
- Jinja2>=3.1
|
||||||
|
pass_filenames: false
|
||||||
|
files: ^roles/k3s_server_post/templates/metallb\.crs\.j2$|^\.github/scripts/test-metallb-interfaces\.py$
|
||||||
- id: metallb-deploy-condition-test
|
- id: metallb-deploy-condition-test
|
||||||
name: MetalLB deploy condition test
|
name: MetalLB deploy condition test
|
||||||
entry: python3 .github/scripts/test-metallb-deploy-condition.py
|
entry: python3 .github/scripts/test-metallb-deploy-condition.py
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ Supported processor architectures are:
|
|||||||
- Server and agent nodes should support passwordless SSH access. Otherwise, pass `--ask-pass --ask-become-pass` to
|
- Server and agent nodes should support passwordless SSH access. Otherwise, pass `--ask-pass --ask-become-pass` to
|
||||||
each playbook command.
|
each playbook command.
|
||||||
|
|
||||||
|
- Every node in the cluster must have a **unique hostname**. k3s registers each node keyed by its hostname, so
|
||||||
|
two nodes with the same hostname cannot join the cluster. `site.yml` asserts this up front and fails fast if
|
||||||
|
any duplicate is found.
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### 🍴 Preparation
|
### 🍴 Preparation
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ cilium_mode: native # native when nodes are on the same subnet or use BGP, other
|
|||||||
cilium_tag: v1.20.0 # cilium version tag
|
cilium_tag: v1.20.0 # cilium version tag
|
||||||
cilium_cli_tag: v0.19.7 # cilium cli version tag
|
cilium_cli_tag: v0.19.7 # cilium cli version tag
|
||||||
cilium_hubble: true # enable hubble observability relay and ui
|
cilium_hubble: true # enable hubble observability relay and ui
|
||||||
|
cilium_envoy: true # enable the Envoy proxy for Cilium L7 policies
|
||||||
|
|
||||||
|
# disable cilium_envoy to skip the Envoy proxy entirely (e.g. no L7 policies)
|
||||||
|
# cilium_envoy: false
|
||||||
|
|
||||||
# if using calico or cilium, you may specify the cluster pod cidr pool
|
# if using calico or cilium, you may specify the cluster pod cidr pool
|
||||||
cluster_cidr: 10.52.0.0/16
|
cluster_cidr: 10.52.0.0/16
|
||||||
@@ -116,6 +120,12 @@ metal_lb_controller_tag_version: v0.16.0
|
|||||||
# metallb ip range for load balancer
|
# metallb ip range for load balancer
|
||||||
metal_lb_ip_range: 192.168.30.80-192.168.30.90
|
metal_lb_ip_range: 192.168.30.80-192.168.30.90
|
||||||
|
|
||||||
|
# (optional) limit MetalLB layer2 announcements to specific network interfaces.
|
||||||
|
# Leave empty (default) to announce on all interfaces.
|
||||||
|
# metal_lb_interfaces:
|
||||||
|
# - eth1
|
||||||
|
# - eth2
|
||||||
|
|
||||||
# Only enable if your nodes are proxmox LXC nodes, make sure to configure your proxmox nodes
|
# Only enable if your nodes are proxmox LXC nodes, make sure to configure your proxmox nodes
|
||||||
# in your hosts.ini file.
|
# in your hosts.ini file.
|
||||||
# Please read https://gist.github.com/triangletodd/02f595cd4c0dc9aac5f7763ca2264185 before using this.
|
# Please read https://gist.github.com/triangletodd/02f595cd4c0dc9aac5f7763ca2264185 before using this.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ cilium_bgp_peer_asn: 64512
|
|||||||
cilium_bgp_neighbors: []
|
cilium_bgp_neighbors: []
|
||||||
cilium_bgp_neighbors_groups: ['k3s_all']
|
cilium_bgp_neighbors_groups: ['k3s_all']
|
||||||
cilium_bgp_lb_cidr: 192.168.31.0/24
|
cilium_bgp_lb_cidr: 192.168.31.0/24
|
||||||
|
cilium_envoy: true
|
||||||
cilium_hubble: true
|
cilium_hubble: true
|
||||||
cilium_mode: native
|
cilium_mode: native
|
||||||
cilium_tag: v1.20.0
|
cilium_tag: v1.20.0
|
||||||
@@ -38,4 +39,5 @@ group_name_master: master
|
|||||||
metal_lb_mode: layer2
|
metal_lb_mode: layer2
|
||||||
metal_lb_available_timeout: 240s
|
metal_lb_available_timeout: 240s
|
||||||
metal_lb_controller_tag_version: v0.16.0
|
metal_lb_controller_tag_version: v0.16.0
|
||||||
|
metal_lb_interfaces: []
|
||||||
metal_lb_ip_range: 192.168.30.80-192.168.30.90
|
metal_lb_ip_range: 192.168.30.80-192.168.30.90
|
||||||
|
|||||||
@@ -141,6 +141,14 @@ argument_specs:
|
|||||||
description: MetalLB ip range for load balancer
|
description: MetalLB ip range for load balancer
|
||||||
default: 192.168.30.80-192.168.30.90
|
default: 192.168.30.80-192.168.30.90
|
||||||
|
|
||||||
|
metal_lb_interfaces:
|
||||||
|
description: >-
|
||||||
|
List of network interfaces on which MetalLB should announce the
|
||||||
|
load balancer IPs in layer2 mode. When empty (default), MetalLB
|
||||||
|
announces on all interfaces.
|
||||||
|
type: list
|
||||||
|
default: []
|
||||||
|
|
||||||
metal_lb_controller_tag_version:
|
metal_lb_controller_tag_version:
|
||||||
description: Image tag for MetalLB
|
description: Image tag for MetalLB
|
||||||
default: v0.16.0
|
default: v0.16.0
|
||||||
|
|||||||
@@ -178,6 +178,7 @@
|
|||||||
--helm-set hubble.enabled={{ "true" if cilium_hubble else "false" }}
|
--helm-set hubble.enabled={{ "true" if cilium_hubble else "false" }}
|
||||||
--helm-set hubble.relay.enabled={{ "true" if cilium_hubble else "false" }}
|
--helm-set hubble.relay.enabled={{ "true" if cilium_hubble else "false" }}
|
||||||
--helm-set hubble.ui.enabled={{ "true" if cilium_hubble else "false" }}
|
--helm-set hubble.ui.enabled={{ "true" if cilium_hubble else "false" }}
|
||||||
|
--helm-set envoy.enabled={{ "true" if cilium_envoy else "false" }}
|
||||||
{% if kube_proxy_replacement is not false %}
|
{% if kube_proxy_replacement is not false %}
|
||||||
--helm-set loadBalancer.algorithm={{ bpf_lb_algorithm }}
|
--helm-set loadBalancer.algorithm={{ bpf_lb_algorithm }}
|
||||||
--helm-set loadBalancer.mode={{ bpf_lb_mode }}
|
--helm-set loadBalancer.mode={{ bpf_lb_mode }}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ kind: L2Advertisement
|
|||||||
metadata:
|
metadata:
|
||||||
name: default
|
name: default
|
||||||
namespace: metallb-system
|
namespace: metallb-system
|
||||||
|
{% if metal_lb_interfaces | default([]) | length > 0 %}
|
||||||
|
spec:
|
||||||
|
interfaces:{% for iface in metal_lb_interfaces %}
|
||||||
|
- {{ iface }}{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if metal_lb_mode == "bgp" %}
|
{% if metal_lb_mode == "bgp" %}
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -8,6 +8,23 @@
|
|||||||
msg: >
|
msg: >
|
||||||
"Ansible is out of date. See here for more info: https://docs.technotim.com/posts/ansible-automation/"
|
"Ansible is out of date. See here for more info: https://docs.technotim.com/posts/ansible-automation/"
|
||||||
|
|
||||||
|
- name: Verify all cluster nodes have unique hostnames
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that: (cluster_hostnames | unique | length) == (cluster_hostnames | length)
|
||||||
|
msg: >-
|
||||||
|
k3s nodes must have unique hostnames. Found a duplicate in:
|
||||||
|
{{ cluster_hostnames | unique }}. Each node registers in the cluster
|
||||||
|
keyed by its hostname, so matching hostnames prevent nodes from joining.
|
||||||
|
vars:
|
||||||
|
cluster_hostnames: >-
|
||||||
|
{{
|
||||||
|
groups['k3s_cluster']
|
||||||
|
| map('extract', hostvars, 'ansible_hostname')
|
||||||
|
| list
|
||||||
|
}}
|
||||||
|
run_once: true
|
||||||
|
when: "'k3s_cluster' in groups"
|
||||||
|
|
||||||
- name: Prepare Proxmox cluster
|
- name: Prepare Proxmox cluster
|
||||||
hosts: proxmox
|
hosts: proxmox
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
|
|||||||
Reference in New Issue
Block a user