Compare commits

..
1 Commits
Author SHA1 Message Date
Timothy Stewart e0ac53dba3 fix(metallb): verify the metallb-system namespace actually exists
- change the Test metallb-system namespace task to run
  k3s kubectl get namespace metallb-system instead of the bare
  -n metallb-system, which printed kubectl usage and always exited 0
- add a regression test that asserts the task uses an explicit get
  and fails if it ever regresses to the usage-only form
- wire the new test into pre-commit
2026-08-06 09:08:11 -05:00
32 changed files with 113 additions and 731 deletions

No files matched your search

@@ -1,112 +0,0 @@
#!/usr/bin/env python3
"""Assert the k3s-init log destination guard skips when unset.
``roles/k3s_server/tasks/main.yml`` fetches k3s-init diagnostics only when
``ANSIBLE_K3S_LOG_DIR`` is set. The lookup default must be an empty string:
Jinja stringifies the boolean ``False`` to ``'False'``, which is non-empty, so
a boolean default makes the guard pass and the role creates a directory
literally named ``False`` in the operator's working directory on every run.
This test extracts the real ``log_destination`` expression from the role and
renders it for both the unset and set cases.
"""
from __future__ import print_function
import os
import re
import subprocess
import yaml
from jinja2 import Environment
ROLE = os.path.join("roles", "k3s_server", "tasks", "main.yml")
def repo_root():
return subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
def fail(message):
raise SystemExit("k3s-init log destination test failed: " + message)
def find_log_task(root):
"""Return the 'Save logs of k3s-init.service' task from the role."""
with open(os.path.join(root, ROLE), "r") as handle:
tasks = yaml.safe_load(handle)
def walk(entries):
for entry in entries or []:
if not isinstance(entry, dict):
continue
if "Save logs of k3s-init.service" in str(entry.get("name", "")):
return entry
for key in ("block", "always", "rescue"):
found = walk(entry.get(key))
if found:
return found
return None
task = walk(tasks)
if task is None:
fail("could not find the 'Save logs of k3s-init.service' task in " + ROLE)
return task
def env_lookup(name, default=None, **kwargs):
"""Stand-in for the env lookup: unset names fall back to the default."""
return {"ANSIBLE_K3S_LOG_DIR": None}.get(name, default)
def render(task, env, value):
"""Render log_destination the way Ansible folds a '>-' scalar."""
expression = str(task["vars"]["log_destination"]).strip()
template = env.from_string(expression)
def lookup(*args, **kwargs):
name = args[1] if len(args) > 1 else kwargs.get("name")
if name != "ANSIBLE_K3S_LOG_DIR":
return kwargs.get("default")
return kwargs["default"] if value is None else value
return template.render(lookup=lookup).strip()
def main():
root = repo_root()
task = find_log_task(root)
env = Environment()
unset = render(task, env, None)
if unset != "":
fail(
"log_destination resolved to %r when ANSIBLE_K3S_LOG_DIR is unset; "
"it must be an empty string so the guard skips" % unset
)
if task["when"].strip() == "":
fail("the guard on the log task is empty, so it would always run")
set_path = render(task, env, "/var/log/k3s-init")
if set_path != "/var/log/k3s-init":
fail(
"log_destination resolved to %r when ANSIBLE_K3S_LOG_DIR is set; "
"expected the path to be passed through unchanged" % set_path
)
# A stray default= would leave the boolean in place even though the
# expression still renders empty, so assert the source no longer carries
# it. This is the exact regression that produced the False/ directory.
if re.search(r"default\s*=\s*False", yaml.safe_dump(task)):
fail(
"log_destination still uses a boolean default; Jinja would "
"stringify it to 'False' and create a directory named False"
)
print("k3s-init log destination regression test passed")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -26,7 +26,7 @@ if grep -Fq -- "node-role.kubernetes.io/master=true' -o=jsonpath" "$main_tasks";
exit 1 exit 1
fi fi
grep -Fq -- "map('extract', hostvars, ['ansible_facts', 'hostname'])" "$main_tasks" grep -Fq -- "map('extract', hostvars, 'ansible_hostname')" "$main_tasks"
grep -Fq -- 'difference(nodes.stdout.split())' "$main_tasks" grep -Fq -- 'difference(nodes.stdout.split())' "$main_tasks"
grep -Fq -- 'crd/addons.k3s.cattle.io' "$main_tasks" grep -Fq -- 'crd/addons.k3s.cattle.io' "$main_tasks"
grep -Fq -- 'crd/helmcharts.helm.cattle.io' "$main_tasks" grep -Fq -- 'crd/helmcharts.helm.cattle.io' "$main_tasks"
@@ -1,166 +0,0 @@
#!/usr/bin/env python3
"""Regression test for the kube-vip deploy conditions.
The control-plane VIP (roles/k3s_server/tasks/vip.yml) and the kube-vip
service load balancer (roles/k3s_server/tasks/kube-vip.yml) are both included
from roles/k3s_server/tasks/main.yml. Before this switch existed the VIP
include had no gate and always ran, so a user who wanted no kube-vip (single
node or external LB) could not opt out.
This test loads the real `when` expressions from the k3s_server task file and
evaluates them against representative variable sets, asserting that:
- the control-plane VIP is deployed only when kube_vip_enabled is true;
- the service load balancer is deployed only when kube_vip_enabled is true
AND kube_vip_lb_ip_range is defined.
"""
from __future__ import print_function
import os
import re
import subprocess
import yaml
from jinja2 import Environment
VIP_WHEN = "kube_vip_enabled"
KUBE_VIP_WHEN = "kube_vip_enabled and kube_vip_lb_ip_range is defined"
def repo_root():
return subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
def fail(message):
raise SystemExit("kube-vip deploy condition test failed: " + message)
def extract_when(path, task_name):
"""Return the `when:` expression string for the named task."""
with open(path, encoding="utf-8") as handle:
doc = yaml.safe_load(handle)
for task in doc:
if task.get("name") == task_name:
when = task.get("when")
return (when or "").strip()
return None
def evaluate(when, variables):
"""Evaluate a `when` expression against variables using Jinja2."""
env = Environment()
def fake_bool(value):
# Minimal stand-in for Ansible's truthiness filter used by `| bool`.
if isinstance(value, bool):
return value
if value is None:
return False
return str(value).lower() in ("1", "true", "yes", "on")
env.filters["bool"] = fake_bool
template = env.from_string("{{ " + when + " }}")
rendered = template.render(**variables)
# The expression renders to the literal strings "True"/"False".
if rendered == "True":
return True
if rendered == "False":
return False
fail("condition did not render to a boolean: {0!r}".format(rendered))
def assert_deployment(when, variables, expected, label):
result = evaluate(when, variables)
verdict = "deploy" if result else "skip"
expected_verdict = "deploy" if expected else "skip"
if result != expected:
fail(
"{0}: expected to {1} but the condition chose to {2} "
"(vars: {3})".format(label, expected_verdict, verdict, variables)
)
def scenarios():
"""Yield (variables, expected_vip, expected_service_lb, label) pairs."""
yield (
# Default inventory: kube-vip enabled, no service LB range.
{
"kube_vip_enabled": True,
},
True,
False,
"default: kube-vip VIP only",
)
yield (
# kube-vip owns the service LB range too.
{
"kube_vip_enabled": True,
"kube_vip_lb_ip_range": "192.168.30.80-192.168.30.90",
},
True,
True,
"kube-vip VIP and service LB",
)
yield (
# Explicitly disabled, no LB range.
{
"kube_vip_enabled": False,
},
False,
False,
"kube_vip_enabled: false",
)
yield (
# Explicitly disabled even when the LB range is present.
{
"kube_vip_enabled": False,
"kube_vip_lb_ip_range": "192.168.30.80-192.168.30.90",
},
False,
False,
"kube_vip_enabled: false despite LB range",
)
def main():
root = repo_root()
server_tasks = os.path.join(root, "roles", "k3s_server", "tasks", "main.yml")
vip_when = extract_when(server_tasks, "Deploy vip manifest")
kube_vip_when = extract_when(server_tasks, "Deploy kube-vip manifest")
if vip_when is None:
fail("could not find 'Deploy vip manifest' when condition")
if kube_vip_when is None:
fail("could not find 'Deploy kube-vip manifest' when condition")
if vip_when != VIP_WHEN:
fail(
"k3s_server/tasks/main.yml 'Deploy vip manifest' when condition "
"changed unexpectedly:\n"
" expected: {0}\n got: {1}".format(VIP_WHEN, vip_when)
)
if kube_vip_when != KUBE_VIP_WHEN:
fail(
"k3s_server/tasks/main.yml 'Deploy kube-vip manifest' when "
"condition changed unexpectedly:\n"
" expected: {0}\n got: {1}".format(KUBE_VIP_WHEN, kube_vip_when)
)
for variables, expected_vip, expected_service_lb, label in scenarios():
assert_deployment(vip_when, variables, expected_vip, "vip " + label)
assert_deployment(
kube_vip_when,
variables,
expected_service_lb,
"service_lb " + label,
)
print("kube-vip deploy condition regression test passed for all scenarios")
if __name__ == "__main__":
main()
+4 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Render the kube-vip DaemonSet template and assert env key correctness. """Render the kube-vip DaemonSet template and assert env key correctness.
kube-vip v1.2.3 reads `bgp_peers` and `vip_subnet`; it ignores the older kube-vip v1.2.2 reads `bgp_peers` and `vip_subnet`; it ignores the older
`bgppeers` and `vip_cidr` names. This test proves the rendered manifest uses `bgppeers` and `vip_cidr` names. This test proves the rendered manifest uses
the keys the target image actually parses. the keys the target image actually parses.
""" """
@@ -74,7 +74,7 @@ def render(env, extra_vars):
{"peer_address": "192.168.30.1", "peer_asn": "64512"}, {"peer_address": "192.168.30.1", "peer_asn": "64512"},
{"peer_address": "192.168.30.2", "peer_asn": "64513"}, {"peer_address": "192.168.30.2", "peer_asn": "64513"},
], ],
"kube_vip_tag_version": "v1.2.3", "kube_vip_tag_version": "v1.2.2",
} }
base_vars.update(extra_vars) base_vars.update(extra_vars)
template = env.get_template("vip.yaml.j2") template = env.get_template("vip.yaml.j2")
@@ -105,8 +105,8 @@ def main():
fail("rendered manifest still uses the ignored vip_cidr key") fail("rendered manifest still uses the ignored vip_cidr key")
if "192.168.30.1:64512,192.168.30.2:64513" not in output: if "192.168.30.1:64512,192.168.30.2:64513" not in output:
fail("bgp_peers value is not comma-separated address:ASN entries") fail("bgp_peers value is not comma-separated address:ASN entries")
if "ghcr.io/kube-vip/kube-vip:v1.2.3" not in output: if "ghcr.io/kube-vip/kube-vip:v1.2.2" not in output:
fail("kube-vip image tag is not v1.2.3") fail("kube-vip image tag is not v1.2.2")
# BGP enabled with no merged peers: single-peer fallback vars, no bgp_peers. # BGP enabled with no merged peers: single-peer fallback vars, no bgp_peers.
output = render( output = render(
@@ -10,8 +10,7 @@ scenario.
This test loads the real `when` expressions from both task files and evaluates This test loads the real `when` expressions from both task files and evaluates
them against representative variable sets, asserting MetalLB is deployed in them against representative variable sets, asserting MetalLB is deployed in
every topology except when MetalLB is explicitly disabled, kube-vip owns the every topology except when kube-vip owns the VIP range or Cilium BGP is enabled.
VIP range, or Cilium BGP is enabled.
""" """
from __future__ import print_function from __future__ import print_function
@@ -24,7 +23,6 @@ import yaml
from jinja2 import Environment from jinja2 import Environment
METALLB_WHEN = ( METALLB_WHEN = (
"metal_lb_enabled and "
"kube_vip_lb_ip_range is not defined and " "kube_vip_lb_ip_range is not defined and "
"not (cilium_bgp | default(false) | bool)" "not (cilium_bgp | default(false) | bool)"
) )
@@ -90,7 +88,6 @@ def scenarios():
yield ( yield (
# Default Flannel inventory (all.yml sets cilium_bgp: false). # Default Flannel inventory (all.yml sets cilium_bgp: false).
{ {
"metal_lb_enabled": True,
"cilium_bgp": False, "cilium_bgp": False,
"cilium_iface": None, "cilium_iface": None,
}, },
@@ -101,7 +98,6 @@ def scenarios():
# Calico CNI with no Cilium variable in scope (issue #644): cilium_bgp # Calico CNI with no Cilium variable in scope (issue #644): cilium_bgp
# is genuinely undefined, so `default(false)` must keep MetalLB on. # is genuinely undefined, so `default(false)` must keep MetalLB on.
{ {
"metal_lb_enabled": True,
"calico_iface": "eth1", "calico_iface": "eth1",
}, },
True, True,
@@ -110,7 +106,6 @@ def scenarios():
yield ( yield (
# Cilium CNI with BGP disabled: MetalLB must still be deployed. # Cilium CNI with BGP disabled: MetalLB must still be deployed.
{ {
"metal_lb_enabled": True,
"cilium_bgp": False, "cilium_bgp": False,
"cilium_iface": "eth1", "cilium_iface": "eth1",
}, },
@@ -120,7 +115,6 @@ def scenarios():
yield ( yield (
# Cilium CNI with BGP enabled: Cilium provides the LB, skip MetalLB. # Cilium CNI with BGP enabled: Cilium provides the LB, skip MetalLB.
{ {
"metal_lb_enabled": True,
"cilium_bgp": True, "cilium_bgp": True,
"cilium_iface": "eth1", "cilium_iface": "eth1",
}, },
@@ -130,21 +124,12 @@ def scenarios():
yield ( yield (
# kube-vip is the load balancer provider: skip MetalLB. # kube-vip is the load balancer provider: skip MetalLB.
{ {
"metal_lb_enabled": True,
"kube_vip_lb_ip_range": "192.168.30.80-192.168.30.90", "kube_vip_lb_ip_range": "192.168.30.80-192.168.30.90",
"cilium_bgp": False, "cilium_bgp": False,
}, },
False, False,
"kube-vip owns the VIP range", "kube-vip owns the VIP range",
) )
yield (
# MetalLB explicitly disabled (external LB / single node): skip.
{
"metal_lb_enabled": False,
},
False,
"metal_lb_enabled: false (external LB)",
)
def main(): def main():
+34 -70
View File
@@ -1,20 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Regression test for the MetalLB converge checks. """Regression test for the MetalLB namespace existence check.
The MetalLB tasks in roles/k3s_server_post/tasks/metallb.yml must actually The "Test metallb-system namespace" task in
verify resources through an explicit kubectl get, and must retry on a roles/k3s_server_post/tasks/metallb.yml must actually verify the namespace
transient kube API error while MetalLB converges. exists. A previous version ran `k3s kubectl -n metallb-system` with no
subcommand, which only printed a usage page and always exited 0, so the task
always succeeded even when the namespace did not exist (issue #350).
The "Test metallb-system namespace" task previously ran `k3s kubectl -n This test loads the real task and asserts the command performs an explicit
metallb-system` with no subcommand, which only printed a usage page and always `get namespace metallb-system`, which returns non-zero when the namespace is
exited 0, so it always succeeded even when the namespace did not exist (issue absent.
#350). It must instead run an explicit `get namespace metallb-system`, which
returns non-zero when the namespace is absent.
An explicit get actually contacts the API server, so these tasks need the same
retry wiring as their siblings (register, until rc == 0, retries, delay). A
bare get with no retry would otherwise abort the converge play on a transient
kube API error while MetalLB converges.
""" """
from __future__ import print_function from __future__ import print_function
@@ -32,53 +27,9 @@ def repo_root():
def fail(message): def fail(message):
raise SystemExit("MetalLB namespace test failed: " + message) raise SystemExit(
"MetalLB namespace test failed: " + message
)
def find_task(tasks, name):
for entry in tasks:
if entry.get("name") == name:
return entry
fail("could not find the '{0}' task".format(name))
return None
def command_text(task):
cmd = task.get("ansible.builtin.command")
if not cmd:
cmd = task.get("command")
if not cmd:
fail("task does not use ansible.builtin.command")
return cmd if isinstance(cmd, str) else " ".join(cmd)
def check_explicit_get(task, name, needle):
text = command_text(task)
if needle not in text:
fail(
"command does not run '{0}'; the task would only print usage and "
"never verify the resource (got: {1!r})".format(needle, text)
)
def check_retry_wiring(task, name):
# The sibling k3s_server_post metallb tasks retry kubectl because the kube
# API can briefly be unavailable while MetalLB converges. Without the same
# retry, a transient API error aborts the whole converge play.
if not task.get("register"):
fail(
"{0} does not register a result; without retry wiring a transient "
"kube API error aborts the converge play".format(name)
)
if not isinstance(task.get("until"), str) or "rc == 0" not in task["until"]:
fail(
"{0} does not retry on rc == 0; the kube API can transiently fail "
"while MetalLB converges and abort the play".format(name)
)
if task.get("retries") is None:
fail("{0} is missing retries".format(name))
if task.get("delay") is None:
fail("{0} is missing delay".format(name))
def main(): def main():
@@ -88,18 +39,31 @@ def main():
with open(task_file, encoding="utf-8") as handle: with open(task_file, encoding="utf-8") as handle:
tasks = yaml.safe_load(handle) tasks = yaml.safe_load(handle)
namespace_task = find_task(tasks, "Test metallb-system namespace") task = None
for entry in tasks:
if entry.get("name") == "Test metallb-system namespace":
task = entry
break
if task is None:
fail("could not find the 'Test metallb-system namespace' task")
cmd = task.get("ansible.builtin.command")
if not cmd:
cmd = task.get("command")
if not cmd:
fail("task does not use ansible.builtin.command")
command_text = cmd if isinstance(cmd, str) else " ".join(cmd)
# A bare `-n metallb-system` with no subcommand prints kubectl usage and # A bare `-n metallb-system` with no subcommand prints kubectl usage and
# always exits 0, so it never proves the namespace exists. The fix must # always exits 0, so it never proves the namespace exists. The fix must
# use an explicit get. # use an explicit get.
check_explicit_get(namespace_task, "Test metallb-system namespace", if "get namespace metallb-system" not in command_text:
"get namespace metallb-system") fail(
check_retry_wiring(namespace_task, "Test metallb-system namespace") "command does not run 'get namespace metallb-system'; "
"the task would only print usage and never verify the namespace "
webhook_task = find_task(tasks, "Test metallb-system webhook-service endpoint") "(got: {0!r})".format(command_text)
check_explicit_get(webhook_task, "Test metallb-system webhook-service endpoint", )
"get endpoints")
check_retry_wiring(webhook_task, "Test metallb-system webhook-service endpoint")
print("MetalLB namespace check regression test passed") print("MetalLB namespace check regression test passed")
@@ -1,69 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repo_root="$(git rev-parse --show-toplevel)"
tasks_file="$repo_root/roles/raspberrypi/tasks/main.yml"
if [ ! -f "$tasks_file" ]; then
printf 'cannot find %s\n' "$tasks_file" >&2
exit 1
fi
# The role decides whether a host is a Raspberry Pi from the return code of two
# ansible.builtin.command probes. The command module never executes under
# --check: it returns rc 0 with "Command would have run if not in check mode".
# Without check_mode: false on both probes every host looks like a Raspberry Pi
# in a check run, so distro specific tasks (e.g. the Ubuntu cgroup
# cmdline.txt edit) run against hosts that must not be touched.
probe_count="$(grep -c -- 'check_mode: false' "$tasks_file" || true)"
if [ "$probe_count" -lt 2 ]; then
printf 'raspberrypi detection probes must set check_mode: false (found %s, expected 2)\n' \
"$probe_count" >&2
exit 1
fi
# Both probes must be the detection greps, not some other task.
for probe in \
'grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo' \
'grep -E "Raspberry Pi" /proc/device-tree/model'; do
grep -Fq -- "$probe" "$tasks_file" || {
printf 'missing raspberry pi detection probe: %s\n' "$probe" >&2
exit 1
}
done
# The probes must stay registered under the names the gate consumes.
for var in grep_cpuinfo_raspberrypi grep_device_tree_model_raspberrypi; do
grep -Fq -- "register: $var" "$tasks_file" || {
printf 'detection probe is not registered as %s\n' "$var" >&2
exit 1
}
done
# The gate must keep keying on rc 0 and must tolerate a missing rc so an
# undefined result cannot be read as "this host is a Raspberry Pi".
grep -Eq -- 'when:.*grep_cpuinfo_raspberrypi\.rc.*== 0.*grep_device_tree_model_raspberrypi\.rc.*== 0' \
"$tasks_file" || {
printf 'raspberry_pi gate no longer tests both probe return codes for 0\n' >&2
exit 1
}
grep -Eq -- 'grep_cpuinfo_raspberrypi\.rc \| default\(1\)' "$tasks_file" || {
printf 'raspberry_pi gate does not default the /proc/cpuinfo probe rc\n' >&2
exit 1
}
grep -Eq -- 'grep_device_tree_model_raspberrypi\.rc \| default\(1\)' "$tasks_file" || {
printf 'raspberry_pi gate does not default the /proc/device-tree probe rc\n' >&2
exit 1
}
# The distro specific include must remain gated on the detection result so a
# non Raspberry Pi host skips setup/ and teardown/ entirely.
grep -Eq -- 'raspberry_pi\|default\(false\)' "$tasks_file" || {
printf 'OS specific tasks are no longer gated on raspberry_pi\n' >&2
exit 1
}
printf 'Raspberry Pi detection regression test passed\n'
@@ -1,92 +0,0 @@
#!/usr/bin/env python3
"""Regression test for the verify_from_outside nginx HTTP assert.
The "Assert that the nginx welcome page is available" task in
molecule/resources/verify_from_outside/tasks/test/deploy-example.yml performs
a plain HTTP GET (ansible.builtin.uri) against the MetalLB VIP that the load
balancer pool assigned, reaching it from the CI runner over the VirtualBox
host-only network.
The MetalLB speaker announces the VIP on the host-only network, and the runner
may briefly not see it in its ARP table yet. That manifests as a single-shot
"status -1 / No route to host" failure that fails the whole verify play even
though the cluster is healthy. Like the sibling load-balancer address wait and
the k3s_server_post metallb waits, the assert must retry until it observes
HTTP 200 with the welcome page.
A bare assert with no retry would otherwise fail an otherwise-green cluster on
a transient announcement miss.
"""
from __future__ import print_function
import os
import subprocess
import yaml
def repo_root():
return subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
def fail(message):
raise SystemExit("verify nginx assert test failed: " + message)
def find_task(play, name):
block = play.get("block")
if not isinstance(block, list):
fail("expected a block list of tasks")
for entry in block:
if entry.get("name") == name:
return entry
fail("could not find the '{0}' task".format(name))
return None
def check_retry_wiring(task, name):
if not task.get("register"):
fail("{0} does not register a result; without retry wiring a transient "
"MetalLB VIP announcement miss fails the verify play".format(name))
until = task.get("until")
if not until:
fail("{0} does not retry; a transient MetalLB VIP announcement miss "
"on the host-only network would fail the verify play".format(name))
if isinstance(until, str):
until = [until]
joined = " | ".join(str(expr) for expr in until)
if "result.status == 200" not in joined:
fail("{0} does not retry until the HTTP status is 200".format(name))
if "Welcome to nginx!" not in joined:
fail("{0} does not verify the welcome page content".format(name))
if task.get("retries") is None:
fail("{0} is missing retries".format(name))
if task.get("delay") is None:
fail("{0} is missing delay".format(name))
def main():
task_file = os.path.join(
repo_root(),
"molecule", "resources", "verify_from_outside", "tasks", "test",
"deploy-example.yml",
)
with open(task_file, encoding="utf-8") as handle:
plays = yaml.safe_load(handle)
play = plays[0]
task = find_task(play, "Assert that the nginx welcome page is available")
if not task.get("ansible.builtin.uri"):
fail("task does not use ansible.builtin.uri to reach the VIP")
check_retry_wiring(task, "Assert that the nginx welcome page is available")
print("verify nginx assert regression test passed")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1
- name: Ensure SHA pinned actions - name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@62574f011e0d1967d555a862bd28a7abba8684fe # 5.0.9 uses: zgosalvez/github-actions-ensure-sha-pinned-actions@46cfe808a5f1588656ef299eedd0ce2fd7ec0dcc # 5.0.6
with: with:
allowlist: | allowlist: |
aws-actions/ aws-actions/
+4 -35
View File
@@ -23,6 +23,10 @@ repos:
additional_dependencies: [ansible-core==2.18.0] additional_dependencies: [ansible-core==2.18.0]
language_version: python3.12 language_version: python3.12
args: [--offline] args: [--offline]
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4 rev: v1.5.4
hooks: hooks:
@@ -34,12 +38,6 @@ repos:
- id: fix-smartquotes - id: fix-smartquotes
- repo: local - repo: local
hooks: hooks:
- id: shellcheck
name: shellcheck
entry: shellcheck
language: python
additional_dependencies: [shellcheck-py==0.11.0.1]
types: [shell]
- id: cleanup-runner-resources-test - id: cleanup-runner-resources-test
name: cleanup runner resources test name: cleanup runner resources test
entry: .github/scripts/test-cleanup-runner-resources.sh entry: .github/scripts/test-cleanup-runner-resources.sh
@@ -64,12 +62,6 @@ 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: k3s-init-log-destination-test
name: K3s init log destination test
entry: .github/scripts/test-k3s-init-log-destination.py
language: system
pass_filenames: false
files: ^roles/k3s_server/tasks/main\.yml$|^\.github/scripts/test-k3s-init-log-destination\.py$
- id: unique-hostname-precheck-test - id: unique-hostname-precheck-test
name: Unique hostname precheck test name: Unique hostname precheck test
entry: .github/scripts/test-unique-hostname-precheck.sh entry: .github/scripts/test-unique-hostname-precheck.sh
@@ -82,12 +74,6 @@ repos:
language: system language: system
pass_filenames: false pass_filenames: false
files: ^roles/prereq/(tasks/main|defaults/main)\.yml$|^\.github/scripts/test-disable-swap\.sh$ files: ^roles/prereq/(tasks/main|defaults/main)\.yml$|^\.github/scripts/test-disable-swap\.sh$
- id: raspberrypi-detection-test
name: Raspberry Pi detection test
entry: .github/scripts/test-raspberrypi-detection.sh
language: system
pass_filenames: false
files: ^roles/raspberrypi/(tasks/main|defaults/main)\.yml$|^\.github/scripts/test-raspberrypi-detection\.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
@@ -144,15 +130,6 @@ repos:
- PyYAML - PyYAML
pass_filenames: false pass_filenames: false
files: ^roles/k3s_server/tasks/main\.yml$|^roles/k3s_server_post/tasks/main\.yml$|^\.github/scripts/test-metallb-deploy-condition\.py$ # noqa yaml[line-length] files: ^roles/k3s_server/tasks/main\.yml$|^roles/k3s_server_post/tasks/main\.yml$|^\.github/scripts/test-metallb-deploy-condition\.py$ # noqa yaml[line-length]
- id: kube-vip-deploy-condition-test
name: kube-vip deploy condition test
entry: python3 .github/scripts/test-kube-vip-deploy-condition.py
language: python
additional_dependencies:
- Jinja2>=3.1
- PyYAML
pass_filenames: false
files: ^roles/k3s_server/tasks/main\.yml$|^\.github/scripts/test-kube-vip-deploy-condition\.py$ # noqa yaml[line-length]
- id: default-interface-test - id: default-interface-test
name: default interface test name: default interface test
entry: python3 .github/scripts/test-default-interface.py entry: python3 .github/scripts/test-default-interface.py
@@ -161,11 +138,3 @@ repos:
- Jinja2>=3.1 - Jinja2>=3.1
pass_filenames: false pass_filenames: false
files: ^inventory/sample/group_vars/all\.yml$|^\.github/scripts/test-default-interface\.py$ files: ^inventory/sample/group_vars/all\.yml$|^\.github/scripts/test-default-interface\.py$
- id: verify-nginx-assert-test
name: verify nginx assert test
entry: python3 .github/scripts/test-verify-nginx-assert.py
language: python
additional_dependencies:
- PyYAML
pass_filenames: false
files: ^molecule/resources/verify_from_outside/tasks/test/deploy-example\.yml$|^\.github/scripts/test-verify-nginx-assert\.py$ # noqa yaml[line-length]
+1 -3
View File
@@ -220,11 +220,9 @@ See the commands [here](https://technotim.com/posts/k3s-etcd-ansible/#testing-yo
| `k3s_server` | `kube_vip_bgp_peers_groups` | list | `['k3s_master']` | Not required | Inventory group in which to search for additional `kube_vip_bgp_peers` parameters to merge. | | `k3s_server` | `kube_vip_bgp_peers_groups` | list | `['k3s_master']` | Not required | Inventory group in which to search for additional `kube_vip_bgp_peers` parameters to merge. |
| `k3s_server` | `kube_vip_iface` | string | `~` | Not required | Explicitly define an interface that ALL control nodes should use to propagate the VIP, define it here. Otherwise, kube-vip will determine the right interface automatically at runtime. | | `k3s_server` | `kube_vip_iface` | string | `~` | Not required | Explicitly define an interface that ALL control nodes should use to propagate the VIP, define it here. Otherwise, kube-vip will determine the right interface automatically at runtime. |
| `k3s_server` | `kube_vip_endpoint` | string | `~` | Not required | Overrides the internal address kube-vip binds/listens on, which can differ from the announced apiserver_endpoint for complex routing/tunnels. Defaults to apiserver_endpoint. | | `k3s_server` | `kube_vip_endpoint` | string | `~` | Not required | Overrides the internal address kube-vip binds/listens on, which can differ from the announced apiserver_endpoint for complex routing/tunnels. Defaults to apiserver_endpoint. |
| `k3s_server` | `kube_vip_tag_version` | string | `v1.2.3` | Not required | Image tag for kube-vip | | `k3s_server` | `kube_vip_tag_version` | string | `v1.2.2` | Not required | Image tag for kube-vip |
| `k3s_server` | `kube_vip_cloud_provider_tag_version` | string | `v0.0.12` | Not required | Tag for kube-vip-cloud-provider manifest when enable | | `k3s_server` | `kube_vip_cloud_provider_tag_version` | string | `v0.0.12` | Not required | Tag for kube-vip-cloud-provider manifest when enable |
| `k3s_server` | `kube_vip_enabled` | bool | `true` | Not required | Enable kube-vip install, covering both the control-plane VIP and the service load balancer. Set false to skip kube-vip entirely (single node or external LB). |
| `k3s_server`, `k3_server_post` | `kube_vip_lb_ip_range` | string | `~` | Not required | IP range for kube-vip load balancer | | `k3s_server`, `k3_server_post` | `kube_vip_lb_ip_range` | string | `~` | Not required | IP range for kube-vip load balancer |
| `k3s_server`, `k3s_server_post` | `metal_lb_enabled` | bool | `true` | Not required | Enable MetalLB install for service load balancing. Set false to skip MetalLB (external LB). |
| `k3s_server`, `k3s_server_post` | `metal_lb_controller_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB | | `k3s_server`, `k3s_server_post` | `metal_lb_controller_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB |
| `k3s_server` | `metal_lb_speaker_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB | | `k3s_server` | `metal_lb_speaker_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB |
| `k3s_server` | `metal_lb_type` | string | `native` | Not required | Use FRR mode or native. Valid values are `frr` and `native` | | `k3s_server` | `metal_lb_type` | string | `native` | Not required | Use FRR mode or native. Valid values are `frr` and `native` |
+2 -10
View File
@@ -1,5 +1,5 @@
--- ---
k3s_version: v1.36.4+k3s1 k3s_version: v1.36.2+k3s1
# this is the user that has ssh access to these machines # this is the user that has ssh access to these machines
ansible_user: ansibleuser ansible_user: ansibleuser
systemd_dir: /etc/systemd/system systemd_dir: /etc/systemd/system
@@ -102,11 +102,7 @@ extra_agent_args: >-
{{ extra_args }} {{ extra_args }}
# image tag for kube-vip # image tag for kube-vip
kube_vip_tag_version: v1.2.3 kube_vip_tag_version: v1.2.2
# enable kube-vip (covers both the control-plane VIP and service load balancing)
# set false for a single node or when an external load balancer is used
kube_vip_enabled: true
# tag for kube-vip-cloud-provider manifest # tag for kube-vip-cloud-provider manifest
# kube_vip_cloud_provider_tag_version: "v0.0.12" # kube_vip_cloud_provider_tag_version: "v0.0.12"
@@ -115,10 +111,6 @@ kube_vip_enabled: true
# (uncomment to use kube-vip for services instead of MetalLB) # (uncomment to use kube-vip for services instead of MetalLB)
# kube_vip_lb_ip_range: "192.168.30.80-192.168.30.90" # kube_vip_lb_ip_range: "192.168.30.80-192.168.30.90"
# enable MetalLB for service load balancing
# set false when using an external load balancer
metal_lb_enabled: true
# metallb type frr or native # metallb type frr or native
metal_lb_type: native metal_lb_type: native
@@ -69,16 +69,7 @@
url: http://{{ nginx_lb_ip | ansible.utils.ipwrap }}:{{ port_ }}/ url: http://{{ nginx_lb_ip | ansible.utils.ipwrap }}:{{ port_ }}/
return_content: true return_content: true
register: result register: result
# The MetalLB speaker announces the VIP on the host-only network, and the failed_when: "'Welcome to nginx!' not in result.content"
# runner may briefly not see it in its ARP table yet. Retry so a transient
# announcement miss does not fail an otherwise healthy cluster, matching
# the load-balancer address wait above and the k3s_server_post metallb waits.
until:
- result.status is defined
- result.status == 200
- "'Welcome to nginx!' in result.content"
retries: 30
delay: 5
vars: vars:
port_: >- port_: >-
{{ nginx_services.resources[0].spec.ports[0].port }} {{ nginx_services.resources[0].spec.ports[0].port }}
+1 -1
View File
@@ -1,4 +1,4 @@
ansible-core>=2.19.13 ansible-core>=2.19.11
jmespath>=1.1.0 jmespath>=1.1.0
jsonpatch>=1.33 jsonpatch>=1.33
kubernetes>=29.0.0 kubernetes>=29.0.0
+1 -1
View File
@@ -6,7 +6,7 @@
# #
ansible-compat==4.1.11 ansible-compat==4.1.11
# via molecule # via molecule
ansible-core==2.19.13 ansible-core==2.19.11
# via # via
# -r requirements.in # -r requirements.in
# ansible-compat # ansible-compat
+2 -5
View File
@@ -9,7 +9,7 @@ kube_vip_arp: true
kube_vip_iface: kube_vip_iface:
kube_vip_endpoint: kube_vip_endpoint:
kube_vip_cloud_provider_tag_version: v0.0.12 kube_vip_cloud_provider_tag_version: v0.0.12
kube_vip_tag_version: v1.2.3 kube_vip_tag_version: v1.2.2
kube_vip_bgp: false kube_vip_bgp: false
kube_vip_bgp_routerid: 127.0.0.1 kube_vip_bgp_routerid: 127.0.0.1
@@ -20,9 +20,6 @@ kube_vip_bgp_peeras: "64512"
kube_vip_bgp_peers: [] kube_vip_bgp_peers: []
kube_vip_bgp_peers_groups: ['k3s_master'] kube_vip_bgp_peers_groups: ['k3s_master']
kube_vip_enabled: true
metal_lb_enabled: true
metal_lb_controller_tag_version: v0.16.0 metal_lb_controller_tag_version: v0.16.0
metal_lb_speaker_tag_version: v0.16.0 metal_lb_speaker_tag_version: v0.16.0
metal_lb_type: native metal_lb_type: native
@@ -38,7 +35,7 @@ retry_count: 20
# yamllint disable rule:line-length # yamllint disable rule:line-length
server_init_args: >- server_init_args: >-
{% if groups[group_name_master | default('master')] | length > 1 %} {% if groups[group_name_master | default('master')] | length > 1 %}
{% if ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname'] %} {% if ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname'] %}
--cluster-init --cluster-init
{% else %} {% else %}
--server https://{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(",") | first | ansible.utils.ipwrap }}:6443 --server https://{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(",") | first | ansible.utils.ipwrap }}:6443
+1 -16
View File
@@ -89,31 +89,16 @@ argument_specs:
kube_vip_tag_version: kube_vip_tag_version:
description: Image tag for kube-vip description: Image tag for kube-vip
default: v1.2.3 default: v1.2.2
kube_vip_cloud_provider_tag_version: kube_vip_cloud_provider_tag_version:
description: Tag for kube-vip-cloud-provider manifest when enabled description: Tag for kube-vip-cloud-provider manifest when enabled
default: v0.0.12 default: v0.0.12
kube_vip_enabled:
description:
- Enable installing kube-vip.
- Covers both the control-plane VIP and the service load balancer.
- Set false to skip kube-vip entirely, for example on a single node or when an external load balancer is used.
default: true
type: bool
kube_vip_lb_ip_range: kube_vip_lb_ip_range:
description: IP range for kube-vip load balancer description: IP range for kube-vip load balancer
default: ~ default: ~
metal_lb_enabled:
description:
- Enable installing MetalLB for service load balancing.
- Set false to skip MetalLB, for example when an external load balancer is used.
default: true
type: bool
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
@@ -83,7 +83,7 @@
no_log: true no_log: true
register: k3s_input_firewall_rules register: k3s_input_firewall_rules
- name: Ping the primary Kubernetes API address from {{ ansible_facts.hostname }} - name: Ping the primary Kubernetes API address from {{ ansible_hostname }}
ansible.builtin.command: ansible.builtin.command:
argv: argv:
- ping - ping
@@ -97,7 +97,7 @@
no_log: true no_log: true
register: k3s_primary_api_ping register: k3s_primary_api_ping
- name: Probe primary Kubernetes API from {{ ansible_facts.hostname }} - name: Probe primary Kubernetes API from {{ ansible_hostname }}
ansible.builtin.wait_for: ansible.builtin.wait_for:
host: "{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(',') | first }}" host: "{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(',') | first }}"
port: 6443 port: 6443
@@ -122,7 +122,7 @@
become: false become: false
ansible.builtin.template: ansible.builtin.template:
src: content.j2 src: content.j2
dest: "{{ log_destination }}/k3s-init@{{ ansible_facts['hostname'] }}.log" dest: "{{ log_destination }}/k3s-init@{{ ansible_hostname }}.log"
mode: "0644" mode: "0644"
vars: vars:
content: | content: |
@@ -137,7 +137,7 @@
become: false become: false
ansible.builtin.template: ansible.builtin.template:
src: content.j2 src: content.j2
dest: "{{ log_destination }}/network@{{ ansible_facts['hostname'] }}.log" dest: "{{ log_destination }}/network@{{ ansible_hostname }}.log"
mode: "0644" mode: "0644"
vars: vars:
content: | content: |
+1 -1
View File
@@ -25,7 +25,7 @@
ansible.builtin.command: ansible.builtin.command:
cmd: >- cmd: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} get node {{ k3s_kubectl_binary | default('k3s kubectl') }} get node
{{ hostvars[joining_master].ansible_facts['hostname'] }} {{ hostvars[joining_master].ansible_hostname }}
delegate_to: "{{ groups[group_name_master | default('master')][0] }}" delegate_to: "{{ groups[group_name_master | default('master')][0] }}"
register: joined_master register: joined_master
until: joined_master.rc == 0 until: joined_master.rc == 0
+3 -6
View File
@@ -6,8 +6,7 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Download vip cloud provider manifest to first master - name: Download vip cloud provider manifest to first master
ansible.builtin.get_url: ansible.builtin.get_url:
@@ -20,8 +19,7 @@
retries: "{{ download_retries }}" retries: "{{ download_retries }}"
delay: "{{ download_delay }}" delay: "{{ download_delay }}"
until: kube_vip_manifest_download is succeeded until: kube_vip_manifest_download is succeeded
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Copy kubevip configMap manifest to first master - name: Copy kubevip configMap manifest to first master
ansible.builtin.template: ansible.builtin.template:
@@ -30,5 +28,4 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
+16 -23
View File
@@ -24,21 +24,19 @@
- name: Deploy vip manifest - name: Deploy vip manifest
ansible.builtin.include_tasks: vip.yml ansible.builtin.include_tasks: vip.yml
when: kube_vip_enabled
- name: Deploy metallb manifest - name: Deploy metallb manifest
ansible.builtin.include_tasks: metallb.yml ansible.builtin.include_tasks: metallb.yml
tags: metallb tags: metallb
# Deploy MetalLB unless explicitly disabled, kube-vip owns the load balancer # Deploy MetalLB unless kube-vip owns the load balancer IP range, or Cilium
# IP range, or Cilium BGP is enabled (Cilium then provides its own load # BGP is enabled (Cilium then provides its own load balancing). The cilium_bgp
# balancing). The cilium_bgp default keeps this safe when Cilium variables are # default keeps this safe when Cilium variables are not in scope at all (#644)
# not in scope at all (#644) while still deploying MetalLB when a non-BGP # while still deploying MetalLB when a non-BGP Cilium CNI is in use.
# Cilium CNI is in use. when: kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool)
when: metal_lb_enabled and kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool)
- name: Deploy kube-vip manifest - name: Deploy kube-vip manifest
ansible.builtin.include_tasks: kube-vip.yml ansible.builtin.include_tasks: kube-vip.yml
tags: kubevip tags: kubevip
when: kube_vip_enabled and kube_vip_lb_ip_range is defined when: kube_vip_lb_ip_range is defined
- name: Initialize and verify the K3s control plane - name: Initialize and verify the K3s control plane
any_errors_fatal: true any_errors_fatal: true
@@ -104,7 +102,7 @@
expected_control_plane_names: >- expected_control_plane_names: >-
{{ {{
groups[group_name_master | default('master')] groups[group_name_master | default('master')]
| map('extract', hostvars, ['ansible_facts', 'hostname']) | map('extract', hostvars, 'ansible_hostname')
| list | list
}} }}
@@ -124,18 +122,13 @@
always: always:
- name: Save logs of k3s-init.service - name: Save logs of k3s-init.service
ansible.builtin.include_tasks: fetch_k3s_init_logs.yml ansible.builtin.include_tasks: fetch_k3s_init_logs.yml
# The env lookup default must be an empty string, not the boolean # ANSIBLE_K3S_LOG_DIR is a path string when set; evaluate it as a boolean
# False. The `!= ''` comparison below always evaluates to a bool, so # so the conditional is a real boolean (ansible-core 2.19 rejects string
# ansible-core 2.19 never sees a non-boolean conditional and no boolean # conditionals derived from env vars).
# default is needed. With a boolean default, an unset ANSIBLE_K3S_LOG_DIR
# leaves log_destination as the bool False, and `False != ''` is True
# because False == 0. The guard would then always pass, running the
# diagnostics on every provisioning run and creating a directory
# literally named 'False' in the operator's working directory.
when: log_destination | default('') != '' when: log_destination | default('') != ''
vars: vars:
log_destination: >- log_destination: >-
{{ lookup('ansible.builtin.env', 'ANSIBLE_K3S_LOG_DIR', default='') }} {{ lookup('ansible.builtin.env', 'ANSIBLE_K3S_LOG_DIR', default=False) }}
- name: Kill the temporary service used for initialization - name: Kill the temporary service used for initialization
ansible.builtin.systemd: ansible.builtin.systemd:
name: k3s-init name: k3s-init
@@ -188,24 +181,24 @@
- name: Create directory .kube - name: Create directory .kube
ansible.builtin.file: ansible.builtin.file:
path: "{{ ansible_facts['user_dir'] }}/.kube" path: "{{ ansible_user_dir }}/.kube"
state: directory state: directory
owner: "{{ ansible_facts['user_id'] }}" owner: "{{ ansible_user_id }}"
mode: u=rwx,g=rx,o= mode: u=rwx,g=rx,o=
- name: Copy config file to user home directory - name: Copy config file to user home directory
ansible.builtin.copy: ansible.builtin.copy:
src: /etc/rancher/k3s/k3s.yaml src: /etc/rancher/k3s/k3s.yaml
dest: "{{ ansible_facts['user_dir'] }}/.kube/config" dest: "{{ ansible_user_dir }}/.kube/config"
remote_src: true remote_src: true
owner: "{{ ansible_facts['user_id'] }}" owner: "{{ ansible_user_id }}"
mode: u=rw,g=,o= mode: u=rw,g=,o=
- name: Configure kubectl cluster to {{ endpoint_url }} - name: Configure kubectl cluster to {{ endpoint_url }}
ansible.builtin.command: >- ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} config set-cluster default {{ k3s_kubectl_binary | default('k3s kubectl') }} config set-cluster default
--server={{ endpoint_url }} --server={{ endpoint_url }}
--kubeconfig {{ ansible_facts['user_dir'] }}/.kube/config --kubeconfig {{ ansible_user_dir }}/.kube/config
changed_when: true changed_when: true
vars: vars:
endpoint_url: >- endpoint_url: >-
+6 -12
View File
@@ -6,8 +6,7 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: "Download to first master: manifest for metallb-{{ metal_lb_type }}" - name: "Download to first master: manifest for metallb-{{ metal_lb_type }}"
ansible.builtin.get_url: ansible.builtin.get_url:
@@ -20,8 +19,7 @@
retries: "{{ download_retries }}" retries: "{{ download_retries }}"
delay: "{{ download_delay }}" delay: "{{ download_delay }}"
until: metallb_manifest_download is succeeded until: metallb_manifest_download is succeeded
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Set image versions in manifest for metallb-{{ metal_lb_type }} - name: Set image versions in manifest for metallb-{{ metal_lb_type }}
ansible.builtin.replace: ansible.builtin.replace:
@@ -33,15 +31,13 @@
to: metallb/speaker:{{ metal_lb_speaker_tag_version }} to: metallb/speaker:{{ metal_lb_speaker_tag_version }}
loop_control: loop_control:
label: "{{ item.change }} => {{ item.to }}" label: "{{ item.change }} => {{ item.to }}"
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Read back MetalLB manifest from first master - name: Read back MetalLB manifest from first master
ansible.builtin.slurp: ansible.builtin.slurp:
src: /var/lib/rancher/k3s/server/manifests/metallb-crds.yaml src: /var/lib/rancher/k3s/server/manifests/metallb-crds.yaml
register: metallb_manifest register: metallb_manifest
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Check the MetalLB speaker image reference is present - name: Check the MetalLB speaker image reference is present
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -50,8 +46,7 @@
('quay.io/metallb/speaker:' ~ metal_lb_speaker_tag_version) in ('quay.io/metallb/speaker:' ~ metal_lb_speaker_tag_version) in
(metallb_manifest.content | default('') | b64decode) (metallb_manifest.content | default('') | b64decode)
}} }}
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Fail if MetalLB speaker tag was not applied to the manifest - name: Fail if MetalLB speaker tag was not applied to the manifest
ansible.builtin.fail: ansible.builtin.fail:
@@ -61,6 +56,5 @@
was not found in the downloaded manifest. The upstream image was not found in the downloaded manifest. The upstream image
reference may have changed. reference may have changed.
when: when:
- >- - ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- not metallb_manifest_has_speaker_tag - not metallb_manifest_has_speaker_tag
+3 -6
View File
@@ -10,8 +10,7 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Copy kube-vip RBAC manifest to first master - name: Copy kube-vip RBAC manifest to first master
ansible.builtin.template: ansible.builtin.template:
@@ -20,8 +19,7 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Copy vip manifest to first master - name: Copy vip manifest to first master
ansible.builtin.template: ansible.builtin.template:
@@ -30,5 +28,4 @@
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
-1
View File
@@ -39,6 +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_enabled: true
metal_lb_interfaces: [] 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
-7
View File
@@ -133,13 +133,6 @@ argument_specs:
description: IP range for kube-vip load balancer description: IP range for kube-vip load balancer
default: ~ default: ~
metal_lb_enabled:
description:
- Enable installing the MetalLB pool for service load balancing.
- Set false to skip MetalLB, for example when an external load balancer is used.
default: true
type: bool
metal_lb_available_timeout: metal_lb_available_timeout:
description: Wait for MetalLB resources description: Wait for MetalLB resources
default: 240s default: 240s
+1 -2
View File
@@ -1,7 +1,6 @@
--- ---
- name: Deploy Calico to cluster - name: Deploy Calico to cluster
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
run_once: true run_once: true
block: block:
- name: Create manifests directory on first master - name: Create manifests directory on first master
+2 -2
View File
@@ -184,7 +184,7 @@
--helm-set loadBalancer.mode={{ bpf_lb_mode }} --helm-set loadBalancer.mode={{ bpf_lb_mode }}
{% endif %} {% endif %}
environment: environment:
KUBECONFIG: "{{ ansible_facts['user_dir'] }}/.kube/config" KUBECONFIG: "{{ ansible_user_dir }}/.kube/config"
register: cilium_install_result register: cilium_install_result
changed_when: cilium_install_result.rc == 0 changed_when: cilium_install_result.rc == 0
# cilium install/upgrade fetches the Helm chart from helm.cilium.io, which is # cilium install/upgrade fetches the Helm chart from helm.cilium.io, which is
@@ -227,7 +227,7 @@
- name: Wait for Cilium status to be healthy - name: Wait for Cilium status to be healthy
ansible.builtin.command: cilium status --wait ansible.builtin.command: cilium status --wait
environment: environment:
KUBECONFIG: "{{ ansible_facts['user_dir'] }}/.kube/config" KUBECONFIG: "{{ ansible_user_dir }}/.kube/config"
register: cilium_status register: cilium_status
changed_when: false changed_when: false
until: cilium_status.rc == 0 until: cilium_status.rc == 0
+5 -6
View File
@@ -12,12 +12,11 @@
- name: Deploy metallb pool - name: Deploy metallb pool
ansible.builtin.include_tasks: metallb.yml ansible.builtin.include_tasks: metallb.yml
tags: metallb tags: metallb
# Deploy MetalLB unless explicitly disabled, kube-vip owns the load balancer # Deploy MetalLB unless kube-vip owns the load balancer IP range, or Cilium
# IP range, or Cilium BGP is enabled (Cilium then provides its own load # BGP is enabled (Cilium then provides its own load balancing). The cilium_bgp
# balancing). The cilium_bgp default keeps this safe when Cilium variables are # default keeps this safe when Cilium variables are not in scope at all (#644)
# not in scope at all (#644) while still deploying MetalLB when a non-BGP # while still deploying MetalLB when a non-BGP Cilium CNI is in use.
# Cilium CNI is in use. when: kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool)
when: metal_lb_enabled and kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool)
- name: Remove tmp directory used for manifests - name: Remove tmp directory used for manifests
ansible.builtin.file: ansible.builtin.file:
+2 -14
View File
@@ -3,7 +3,7 @@
ansible.builtin.file: ansible.builtin.file:
path: /tmp/k3s path: /tmp/k3s
state: directory state: directory
owner: "{{ ansible_facts['user_id'] }}" owner: "{{ ansible_user_id }}"
mode: "0755" mode: "0755"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
@@ -33,7 +33,7 @@
ansible.builtin.template: ansible.builtin.template:
src: metallb.crs.j2 src: metallb.crs.j2
dest: /tmp/k3s/metallb-crs.yaml dest: /tmp/k3s/metallb-crs.yaml
owner: "{{ ansible_facts['user_id'] }}" owner: "{{ ansible_user_id }}"
mode: "0755" mode: "0755"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
@@ -42,12 +42,6 @@
ansible.builtin.command: >- ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} get namespace metallb-system {{ k3s_kubectl_binary | default('k3s kubectl') }} get namespace metallb-system
changed_when: false changed_when: false
# The kube API can briefly return ServiceUnavailable while MetalLB converges,
# which would otherwise abort the whole converge play on a transient error.
register: metallb_namespace_result
until: metallb_namespace_result.rc == 0
retries: "{{ download_retries }}"
delay: "{{ download_delay }}"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
@@ -105,12 +99,6 @@
ansible.builtin.command: >- ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} -n metallb-system get endpoints {{ metallb_webhook_service_name }} {{ k3s_kubectl_binary | default('k3s kubectl') }} -n metallb-system get endpoints {{ metallb_webhook_service_name }}
changed_when: false changed_when: false
# The kube API can briefly return ServiceUnavailable while MetalLB converges,
# which would otherwise abort the whole converge play on a transient error.
register: metallb_webhook_result
until: metallb_webhook_result.rc == 0
retries: "{{ download_retries }}"
delay: "{{ download_delay }}"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
+9 -17
View File
@@ -29,7 +29,7 @@
- name: Set SELinux to disabled state - name: Set SELinux to disabled state
ansible.posix.selinux: ansible.posix.selinux:
state: disabled state: disabled
when: ansible_facts.os_family == "RedHat" when: ansible_os_family == "RedHat"
- name: Enable IPv4 forwarding - name: Enable IPv4 forwarding
ansible.posix.sysctl: ansible.posix.sysctl:
@@ -63,39 +63,31 @@
executable: /bin/bash executable: /bin/bash
register: br_netfilter_exists register: br_netfilter_exists
changed_when: false changed_when: false
when: ansible_facts.os_family == "RedHat" when: ansible_os_family == "RedHat"
tags: sysctl tags: sysctl
- name: Install kernel-modules-extra if br_netfilter missing - name: Install kernel-modules-extra if br_netfilter missing
ansible.builtin.yum: ansible.builtin.yum:
name: "kernel-modules-extra-{{ ansible_kernel }}" name: kernel-modules-extra
state: present state: present
update_cache: true update_cache: true
register: kernel_modules_installed register: kernel_modules_installed
when: when:
- ansible_facts.os_family == "RedHat" - ansible_os_family == "RedHat"
- br_netfilter_exists.stdout | int == 0 - br_netfilter_exists.stdout | int == 0
- name: Reboot node if kernel modules package was installed
ansible.builtin.reboot:
msg: "Rebooting to apply new kernel modules package"
reboot_timeout: 300
when:
- ansible_facts.os_family == "RedHat"
- kernel_modules_installed is changed
- name: Add br_netfilter to /etc/modules-load.d/ - name: Add br_netfilter to /etc/modules-load.d/
ansible.builtin.copy: ansible.builtin.copy:
content: br_netfilter content: br_netfilter
dest: /etc/modules-load.d/br_netfilter.conf dest: /etc/modules-load.d/br_netfilter.conf
mode: u=rw,g=,o= mode: u=rw,g=,o=
when: ansible_facts.os_family == "RedHat" when: ansible_os_family == "RedHat"
- name: Load br_netfilter - name: Load br_netfilter
community.general.modprobe: community.general.modprobe:
name: br_netfilter name: br_netfilter
state: present state: present
when: ansible_facts.os_family == "RedHat" when: ansible_os_family == "RedHat"
- name: Set bridge-nf-call-iptables (just to be sure) - name: Set bridge-nf-call-iptables (just to be sure)
ansible.posix.sysctl: ansible.posix.sysctl:
@@ -103,7 +95,7 @@
value: "1" value: "1"
state: present state: present
reload: true reload: true
when: ansible_facts.os_family == "RedHat" when: ansible_os_family == "RedHat"
loop: loop:
- net.bridge.bridge-nf-call-iptables - net.bridge.bridge-nf-call-iptables
- net.bridge.bridge-nf-call-ip6tables - net.bridge.bridge-nf-call-ip6tables
@@ -111,10 +103,10 @@
- name: Add /usr/local/bin to sudo secure_path - name: Add /usr/local/bin to sudo secure_path
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
line: Defaults secure_path = {{ secure_path[ansible_facts.os_family] }} line: Defaults secure_path = {{ secure_path[ansible_os_family] }}
regexp: Defaults(\s)*secure_path(\s)*= regexp: Defaults(\s)*secure_path(\s)*=
state: present state: present
insertafter: EOF insertafter: EOF
path: /etc/sudoers path: /etc/sudoers
validate: visudo -cf %s validate: visudo -cf %s
when: ansible_facts.os_family in [ "RedHat", "Suse" ] when: ansible_os_family in [ "RedHat", "Suse" ]
+3 -10
View File
@@ -1,27 +1,20 @@
--- ---
# The probes below must run in check mode as well. The command module does not
# execute under --check, it returns rc 0 with "Command would have run if not in
# check mode", which would otherwise mark every host as a Raspberry Pi and make
# check runs apply the distro specific tasks. The rc values mean: 0 matched, 1 no
# match, 2 the file is absent (no /proc/device-tree on non ARM systems).
- name: Test for raspberry pi /proc/cpuinfo - name: Test for raspberry pi /proc/cpuinfo
ansible.builtin.command: grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo ansible.builtin.command: grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo
register: grep_cpuinfo_raspberrypi register: grep_cpuinfo_raspberrypi
failed_when: false failed_when: false
changed_when: false changed_when: false
check_mode: false
- name: Test for raspberry pi /proc/device-tree/model - name: Test for raspberry pi /proc/device-tree/model
ansible.builtin.command: grep -E "Raspberry Pi" /proc/device-tree/model ansible.builtin.command: grep -E "Raspberry Pi" /proc/device-tree/model
register: grep_device_tree_model_raspberrypi register: grep_device_tree_model_raspberrypi
failed_when: false failed_when: false
changed_when: false changed_when: false
check_mode: false
- name: Set raspberry_pi fact to true - name: Set raspberry_pi fact to true
ansible.builtin.set_fact: ansible.builtin.set_fact:
raspberry_pi: true raspberry_pi: true
when: (grep_cpuinfo_raspberrypi.rc | default(1)) == 0 or (grep_device_tree_model_raspberrypi.rc | default(1)) == 0 when: grep_cpuinfo_raspberrypi.rc == 0 or grep_device_tree_model_raspberrypi.rc == 0
- name: Set detected_distribution to Raspbian (ARM64 on Raspbian, Debian Buster/Bullseye/Bookworm) - name: Set detected_distribution to Raspbian (ARM64 on Raspbian, Debian Buster/Bullseye/Bookworm)
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -56,8 +49,8 @@
with_first_found: with_first_found:
- "{{ action_ }}/{{ detected_distribution }}-{{ detected_distribution_major_version }}.yml" - "{{ action_ }}/{{ detected_distribution }}-{{ detected_distribution_major_version }}.yml"
- "{{ action_ }}/{{ detected_distribution }}.yml" - "{{ action_ }}/{{ detected_distribution }}.yml"
- "{{ action_ }}/{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml" - "{{ action_ }}/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml"
- "{{ action_ }}/{{ ansible_facts['distribution'] }}.yml" - "{{ action_ }}/{{ ansible_distribution }}.yml"
- "{{ action_ }}/default.yml" - "{{ action_ }}/default.yml"
vars: vars:
action_: >- action_: >-
+4 -8
View File
@@ -19,7 +19,7 @@
cluster_hostnames: >- cluster_hostnames: >-
{{ {{
groups['k3s_cluster'] groups['k3s_cluster']
| map('extract', hostvars, ['ansible_facts', 'hostname']) | map('extract', hostvars, 'ansible_hostname')
| list | list
}} }}
run_once: true run_once: true
@@ -77,13 +77,9 @@
hosts: master hosts: master
environment: "{{ proxy_env | default({}) }}" environment: "{{ proxy_env | default({}) }}"
tasks: tasks:
- name: >- - name: Copying kubeconfig from {{ hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname'] }}
Copying kubeconfig from
{{ hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname'] }}
ansible.builtin.fetch: ansible.builtin.fetch:
src: "{{ ansible_facts['user_dir'] }}/.kube/config" src: "{{ ansible_user_dir }}/.kube/config"
dest: ./kubeconfig dest: ./kubeconfig
flat: true flat: true
when: >- when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
ansible_facts.hostname ==
hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']