Compare commits

...
Author SHA1 Message Date
dependabot[bot] bd4698693a chore(deps): bump jsonpatch from 1.33 to 1.35
Bumps [jsonpatch](https://github.com/stefankoegl/python-json-patch) from 1.33 to 1.35.
- [Release notes](https://github.com/stefankoegl/python-json-patch/releases)
- [Commits](https://github.com/stefankoegl/python-json-patch/compare/v1.33...v1.35)

---
updated-dependencies:
- dependency-name: jsonpatch
  dependency-version: '1.35'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-18 18:45:09 -05:00
dependabot[bot] 32bfb3abba chore(deps): bump actions/upload-artifact from 7.0.1 to 7.0.2
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 7.0.1 to 7.0.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-18 18:45:09 -05:00
dependabot[bot] dbf7cbd038 chore(deps): bump ansible-core from 2.19.13 to 2.19.14
Bumps [ansible-core](https://github.com/ansible/ansible) from 2.19.13 to 2.19.14.
- [Release notes](https://github.com/ansible/ansible/releases)
- [Commits](https://github.com/ansible/ansible/compare/v2.19.13...v2.19.14)

---
updated-dependencies:
- dependency-name: ansible-core
  dependency-version: 2.19.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-18 18:45:09 -05:00
dependabot[bot] 8fff1d1b95 chore(deps): bump zgosalvez/github-actions-ensure-sha-pinned-actions
Bumps [zgosalvez/github-actions-ensure-sha-pinned-actions](https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions) from 5.0.9 to 5.0.10.
- [Release notes](https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions/releases)
- [Commits](https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions/compare/62574f011e0d1967d555a862bd28a7abba8684fe...c4e71056006d29f90204b2d063be337109507d62)

---
updated-dependencies:
- dependency-name: zgosalvez/github-actions-ensure-sha-pinned-actions
  dependency-version: 5.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-18 18:45:09 -05:00
Techno Tim a976f16b21 feat(deps): bump zgosalvez/github-actions-ensure-sha-pinned-actions from 5.0.8 to 5.0.9 (#717) 2026-10-02 22:44:56 +00:00
Marc Palacín 377687cbbe fix(k3s_server): skip k3s-init log collection when the destination is unset (#721)
* fix(k3s_server): skip k3s-init log collection when the destination is unset

The guard on the k3s-init diagnostics include used a boolean default for
the ANSIBLE_K3S_LOG_DIR lookup so that the when conditional would be a real
boolean. That is unnecessary: `log_destination | default('') != ''` is a
comparison and always evaluates to a bool, so ansible-core 2.19 never sees
a non-boolean conditional here.

The boolean default also defeats the guard. With ANSIBLE_K3S_LOG_DIR unset,
log_destination is the bool False, and `False != ''` is True in Python
because False == 0. The include therefore runs on every provisioning run,
executing the journalctl, systemctl, ip, ss, iptables, service_facts, ping
and wait_for diagnostics in fetch_k3s_init_logs.yml, then creating a
directory literally named "False" in the operator's working directory.

Use an empty-string default so the unset case renders "" and the guard
skips. CI did not catch this because .github/workflows/test.yml always
exports ANSIBLE_K3S_LOG_DIR.

Add a regression test that parses the role, renders the real log_destination
expression for both the unset and set cases, and asserts that no boolean
default has been reintroduced.

* chore(site): wrap the over-long kubeconfig fetch conditional
2026-10-02 17:34:55 +00:00
Marc Palacín 1b994f7479 fix(raspberrypi): detect Raspberry Pi hardware in check mode (#719)
- set check_mode: false on the /proc/cpuinfo and /proc/device-tree/model
  probes, since ansible.builtin.command never executes under --check and
  returns rc 0 with "Command would have run if not in check mode"
- default a missing probe rc to 1 in the raspberry_pi gate so an undefined
  result cannot be read as "this host is a Raspberry Pi"
- add a structural regression test and hook it into pre-commit

Without this, site.yml --check marked every x86_64 host as a Raspberry Pi and
applied the distro specific setup tasks, failing on Ubuntu with
"Destination /boot/firmware/cmdline.txt does not exist".
2026-09-30 22:45:00 +00:00
FedelloandTimothy Stewart 8fb1e26c63 fix(ansible): update fact references to remove deprecation warnings (#715)
* chore(ansible): use ansible_facts

* fix(ansible): satisfy lint for fact migration

- wrap nested fact expressions to meet YAML line length limits
- update the bootstrap regression assertion for ansible_facts

* fix(pre-commit): install shellcheck for host architecture

- install shellcheck-py from PyPI in the local hook environment
- pin the platform-aware shellcheck package version

---------

Co-authored-by: Timothy Stewart <timothystewart6@gmail.com>
2026-09-21 17:04:30 +00:00
Timothy Stewart ee3a4a126e chore(deps): bump ansible-core from 2.19.12 to 2.19.13 and zgosalvez/github-actions-ensure-sha-pinned-actions from 5.0.7 to 5.0.8 2026-09-11 10:39:34 -05:00
Marc Palacín 24054385e4 Fix: reboot nodes after installing kernel-modules-extra package (#711)
* fix: nodes reboot after kernel-modules-extra installation

* chore: pre-commit fix
2026-09-05 17:26:52 -05:00
Techno Tim 1a244636ea chore(deps): bump k3s to v1.36.4 (#709)
- Bump k3s_version to v1.36.4+k3s1 in sample group_vars
2026-09-02 00:58:34 -05:00
Timothy Stewart 332f36ceec chore(deps): bump k3s to v1.36.3 and kube-vip to v1.2.3
- Bump k3s_version to v1.36.3+k3s1 in sample group_vars
- Bump kube_vip_tag_version to v1.2.3 in group_vars, server role
  defaults and argument spec, and README variable table
- Update the kube-vip manifest regression test to assert v1.2.3
- Keep MetalLB at v0.16.0: v0.16.1 only exists as a chart-only
  metallb-chart-0.16.1 tag with no application or image release,
  so it must not be used as the controller or speaker image tag
2026-08-14 21:53:28 -05:00
Timothy Stewart c8c3609fde fix(verify): retry the nginx VIP assert on transient ARP miss
- convert the verify_from_outside URI assert to register/until/retries/delay
- retry until the MetalLB VIP returns HTTP 200 with the welcome page
- cover the host-only L2 announcement race that failed CI on the shared runner
- add a regression test asserting the retry wiring and hook it into pre-commit
2026-08-14 18:50:49 -05:00
dependabot[bot] ac77e33be0 chore(deps): bump zgosalvez/github-actions-ensure-sha-pinned-actions
Bumps [zgosalvez/github-actions-ensure-sha-pinned-actions](https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions) from 5.0.6 to 5.0.7.
- [Release notes](https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions/releases)
- [Commits](https://github.com/zgosalvez/github-actions-ensure-sha-pinned-actions/compare/46cfe808a5f1588656ef299eedd0ce2fd7ec0dcc...c5fc58bd0be7a4b94b73ce40250322d5b838a108)

---
updated-dependencies:
- dependency-name: zgosalvez/github-actions-ensure-sha-pinned-actions
  dependency-version: 5.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-14 18:50:49 -05:00
dependabot[bot] 070d7b302b chore(deps): bump ansible-core from 2.19.11 to 2.19.12
Bumps [ansible-core](https://github.com/ansible/ansible) from 2.19.11 to 2.19.12.
- [Release notes](https://github.com/ansible/ansible/releases)
- [Commits](https://github.com/ansible/ansible/compare/v2.19.11...v2.19.12)

---
updated-dependencies:
- dependency-name: ansible-core
  dependency-version: 2.19.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-14 18:50:49 -05:00
Timothy Stewart 1687a3b098 feat(k3s-server): add kube_vip_enabled and metal_lb_enabled switches
- gate the control-plane VIP and kube-vip service LB on kube_vip_enabled
- gate the MetalLB manifest and pool on metal_lb_enabled
- add argument_specs entries in both roles
- document both switches in sample inventory and README
- update metal_lb deploy-condition test and add a kube-vip deploy-condition test
2026-08-14 18:50:49 -05:00
32 changed files with 653 additions and 83 deletions

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
#!/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
fi
grep -Fq -- "map('extract', hostvars, 'ansible_hostname')" "$main_tasks"
grep -Fq -- "map('extract', hostvars, ['ansible_facts', 'hostname'])" "$main_tasks"
grep -Fq -- 'difference(nodes.stdout.split())' "$main_tasks"
grep -Fq -- 'crd/addons.k3s.cattle.io' "$main_tasks"
grep -Fq -- 'crd/helmcharts.helm.cattle.io' "$main_tasks"
@@ -0,0 +1,166 @@
#!/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
"""Render the kube-vip DaemonSet template and assert env key correctness.
kube-vip v1.2.2 reads `bgp_peers` and `vip_subnet`; it ignores the older
kube-vip v1.2.3 reads `bgp_peers` and `vip_subnet`; it ignores the older
`bgppeers` and `vip_cidr` names. This test proves the rendered manifest uses
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.2", "peer_asn": "64513"},
],
"kube_vip_tag_version": "v1.2.2",
"kube_vip_tag_version": "v1.2.3",
}
base_vars.update(extra_vars)
template = env.get_template("vip.yaml.j2")
@@ -105,8 +105,8 @@ def main():
fail("rendered manifest still uses the ignored vip_cidr key")
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")
if "ghcr.io/kube-vip/kube-vip:v1.2.2" not in output:
fail("kube-vip image tag is not v1.2.2")
if "ghcr.io/kube-vip/kube-vip:v1.2.3" not in output:
fail("kube-vip image tag is not v1.2.3")
# BGP enabled with no merged peers: single-peer fallback vars, no bgp_peers.
output = render(
@@ -10,7 +10,8 @@ scenario.
This test loads the real `when` expressions from both task files and evaluates
them against representative variable sets, asserting MetalLB is deployed in
every topology except when kube-vip owns the VIP range or Cilium BGP is enabled.
every topology except when MetalLB is explicitly disabled, kube-vip owns the
VIP range, or Cilium BGP is enabled.
"""
from __future__ import print_function
@@ -23,6 +24,7 @@ import yaml
from jinja2 import Environment
METALLB_WHEN = (
"metal_lb_enabled and "
"kube_vip_lb_ip_range is not defined and "
"not (cilium_bgp | default(false) | bool)"
)
@@ -88,6 +90,7 @@ def scenarios():
yield (
# Default Flannel inventory (all.yml sets cilium_bgp: false).
{
"metal_lb_enabled": True,
"cilium_bgp": False,
"cilium_iface": None,
},
@@ -98,6 +101,7 @@ def scenarios():
# Calico CNI with no Cilium variable in scope (issue #644): cilium_bgp
# is genuinely undefined, so `default(false)` must keep MetalLB on.
{
"metal_lb_enabled": True,
"calico_iface": "eth1",
},
True,
@@ -106,6 +110,7 @@ def scenarios():
yield (
# Cilium CNI with BGP disabled: MetalLB must still be deployed.
{
"metal_lb_enabled": True,
"cilium_bgp": False,
"cilium_iface": "eth1",
},
@@ -115,6 +120,7 @@ def scenarios():
yield (
# Cilium CNI with BGP enabled: Cilium provides the LB, skip MetalLB.
{
"metal_lb_enabled": True,
"cilium_bgp": True,
"cilium_iface": "eth1",
},
@@ -124,12 +130,21 @@ def scenarios():
yield (
# 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",
"cilium_bgp": False,
},
False,
"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():
+69
View File
@@ -0,0 +1,69 @@
#!/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'
@@ -0,0 +1,92 @@
#!/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
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@46cfe808a5f1588656ef299eedd0ce2fd7ec0dcc # 5.0.6
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@c4e71056006d29f90204b2d063be337109507d62 # 5.0.10
with:
allowlist: |
aws-actions/
+1 -1
View File
@@ -109,7 +109,7 @@ jobs:
- name: Upload log files
if: always() # do this even if a step before has failed
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # 7.0.2
with:
name: logs-${{ matrix.scenario }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
+35 -4
View File
@@ -23,10 +23,6 @@ repos:
additional_dependencies: [ansible-core==2.18.0]
language_version: python3.12
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
rev: v1.5.4
hooks:
@@ -38,6 +34,12 @@ repos:
- id: fix-smartquotes
- repo: local
hooks:
- id: shellcheck
name: shellcheck
entry: shellcheck
language: python
additional_dependencies: [shellcheck-py==0.11.0.1]
types: [shell]
- id: cleanup-runner-resources-test
name: cleanup runner resources test
entry: .github/scripts/test-cleanup-runner-resources.sh
@@ -62,6 +64,12 @@ repos:
language: system
pass_filenames: false
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
name: Unique hostname precheck test
entry: .github/scripts/test-unique-hostname-precheck.sh
@@ -74,6 +82,12 @@ repos:
language: system
pass_filenames: false
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
name: Cilium BGP manifest test
entry: python3 .github/scripts/test-cilium-bgp-manifest.py
@@ -130,6 +144,15 @@ repos:
- PyYAML
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]
- 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
name: default interface test
entry: python3 .github/scripts/test-default-interface.py
@@ -138,3 +161,11 @@ repos:
- Jinja2>=3.1
pass_filenames: false
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]
+3 -1
View File
@@ -220,9 +220,11 @@ 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_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_tag_version` | string | `v1.2.2` | Not required | Image tag for kube-vip |
| `k3s_server` | `kube_vip_tag_version` | string | `v1.2.3` | 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_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`, `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` | `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` |
+10 -2
View File
@@ -1,5 +1,5 @@
---
k3s_version: v1.36.2+k3s1
k3s_version: v1.36.4+k3s1
# this is the user that has ssh access to these machines
ansible_user: ansibleuser
systemd_dir: /etc/systemd/system
@@ -102,7 +102,11 @@ extra_agent_args: >-
{{ extra_args }}
# image tag for kube-vip
kube_vip_tag_version: v1.2.2
kube_vip_tag_version: v1.2.3
# 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
# kube_vip_cloud_provider_tag_version: "v0.0.12"
@@ -111,6 +115,10 @@ kube_vip_tag_version: v1.2.2
# (uncomment to use kube-vip for services instead of MetalLB)
# 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
metal_lb_type: native
@@ -69,7 +69,16 @@
url: http://{{ nginx_lb_ip | ansible.utils.ipwrap }}:{{ port_ }}/
return_content: true
register: result
failed_when: "'Welcome to nginx!' not in result.content"
# The MetalLB speaker announces the VIP on the host-only network, and the
# 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:
port_: >-
{{ nginx_services.resources[0].spec.ports[0].port }}
+2 -2
View File
@@ -1,6 +1,6 @@
ansible-core>=2.19.11
ansible-core>=2.19.14
jmespath>=1.1.0
jsonpatch>=1.33
jsonpatch>=1.35
kubernetes>=29.0.0
molecule-plugins[vagrant]
molecule>=6.0.3
+3 -3
View File
@@ -6,7 +6,7 @@
#
ansible-compat==4.1.11
# via molecule
ansible-core==2.19.11
ansible-core==2.19.14
# via
# -r requirements.in
# ansible-compat
@@ -55,9 +55,9 @@ jinja2==3.1.3
# molecule
jmespath==1.1.0
# via -r requirements.in
jsonpatch==1.33
jsonpatch==1.35
# via -r requirements.in
jsonpointer==2.4
jsonpointer==3.2.1
# via jsonpatch
jsonschema==4.21.1
# via
+5 -2
View File
@@ -9,7 +9,7 @@ kube_vip_arp: true
kube_vip_iface:
kube_vip_endpoint:
kube_vip_cloud_provider_tag_version: v0.0.12
kube_vip_tag_version: v1.2.2
kube_vip_tag_version: v1.2.3
kube_vip_bgp: false
kube_vip_bgp_routerid: 127.0.0.1
@@ -20,6 +20,9 @@ kube_vip_bgp_peeras: "64512"
kube_vip_bgp_peers: []
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_speaker_tag_version: v0.16.0
metal_lb_type: native
@@ -35,7 +38,7 @@ retry_count: 20
# yamllint disable rule:line-length
server_init_args: >-
{% if groups[group_name_master | default('master')] | length > 1 %}
{% if ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname'] %}
{% if ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname'] %}
--cluster-init
{% else %}
--server https://{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(",") | first | ansible.utils.ipwrap }}:6443
+16 -1
View File
@@ -89,16 +89,31 @@ argument_specs:
kube_vip_tag_version:
description: Image tag for kube-vip
default: v1.2.2
default: v1.2.3
kube_vip_cloud_provider_tag_version:
description: Tag for kube-vip-cloud-provider manifest when enabled
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:
description: IP range for kube-vip load balancer
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:
description: Image tag for MetalLB
default: v0.16.0
@@ -83,7 +83,7 @@
no_log: true
register: k3s_input_firewall_rules
- name: Ping the primary Kubernetes API address from {{ ansible_hostname }}
- name: Ping the primary Kubernetes API address from {{ ansible_facts.hostname }}
ansible.builtin.command:
argv:
- ping
@@ -97,7 +97,7 @@
no_log: true
register: k3s_primary_api_ping
- name: Probe primary Kubernetes API from {{ ansible_hostname }}
- name: Probe primary Kubernetes API from {{ ansible_facts.hostname }}
ansible.builtin.wait_for:
host: "{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(',') | first }}"
port: 6443
@@ -122,7 +122,7 @@
become: false
ansible.builtin.template:
src: content.j2
dest: "{{ log_destination }}/k3s-init@{{ ansible_hostname }}.log"
dest: "{{ log_destination }}/k3s-init@{{ ansible_facts['hostname'] }}.log"
mode: "0644"
vars:
content: |
@@ -137,7 +137,7 @@
become: false
ansible.builtin.template:
src: content.j2
dest: "{{ log_destination }}/network@{{ ansible_hostname }}.log"
dest: "{{ log_destination }}/network@{{ ansible_facts['hostname'] }}.log"
mode: "0644"
vars:
content: |
+1 -1
View File
@@ -25,7 +25,7 @@
ansible.builtin.command:
cmd: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} get node
{{ hostvars[joining_master].ansible_hostname }}
{{ hostvars[joining_master].ansible_facts['hostname'] }}
delegate_to: "{{ groups[group_name_master | default('master')][0] }}"
register: joined_master
until: joined_master.rc == 0
+6 -3
View File
@@ -6,7 +6,8 @@
owner: root
group: root
mode: "0644"
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Download vip cloud provider manifest to first master
ansible.builtin.get_url:
@@ -19,7 +20,8 @@
retries: "{{ download_retries }}"
delay: "{{ download_delay }}"
until: kube_vip_manifest_download is succeeded
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Copy kubevip configMap manifest to first master
ansible.builtin.template:
@@ -28,4 +30,5 @@
owner: root
group: root
mode: "0644"
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
+23 -16
View File
@@ -24,19 +24,21 @@
- name: Deploy vip manifest
ansible.builtin.include_tasks: vip.yml
when: kube_vip_enabled
- name: Deploy metallb manifest
ansible.builtin.include_tasks: metallb.yml
tags: metallb
# Deploy MetalLB unless kube-vip owns the load balancer IP range, or Cilium
# BGP is enabled (Cilium then provides its own load balancing). The cilium_bgp
# default keeps this safe when Cilium variables are not in scope at all (#644)
# while still deploying MetalLB when a non-BGP Cilium CNI is in use.
when: kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool)
# Deploy MetalLB unless explicitly disabled, kube-vip owns the load balancer
# IP range, or Cilium BGP is enabled (Cilium then provides its own load
# balancing). The cilium_bgp default keeps this safe when Cilium variables are
# not in scope at all (#644) while still deploying MetalLB when a non-BGP
# Cilium CNI is in use.
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
ansible.builtin.include_tasks: kube-vip.yml
tags: kubevip
when: kube_vip_lb_ip_range is defined
when: kube_vip_enabled and kube_vip_lb_ip_range is defined
- name: Initialize and verify the K3s control plane
any_errors_fatal: true
@@ -102,7 +104,7 @@
expected_control_plane_names: >-
{{
groups[group_name_master | default('master')]
| map('extract', hostvars, 'ansible_hostname')
| map('extract', hostvars, ['ansible_facts', 'hostname'])
| list
}}
@@ -122,13 +124,18 @@
always:
- name: Save logs of k3s-init.service
ansible.builtin.include_tasks: fetch_k3s_init_logs.yml
# ANSIBLE_K3S_LOG_DIR is a path string when set; evaluate it as a boolean
# so the conditional is a real boolean (ansible-core 2.19 rejects string
# conditionals derived from env vars).
# The env lookup default must be an empty string, not the boolean
# False. The `!= ''` comparison below always evaluates to a bool, so
# ansible-core 2.19 never sees a non-boolean conditional and no boolean
# 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('') != ''
vars:
log_destination: >-
{{ lookup('ansible.builtin.env', 'ANSIBLE_K3S_LOG_DIR', default=False) }}
{{ lookup('ansible.builtin.env', 'ANSIBLE_K3S_LOG_DIR', default='') }}
- name: Kill the temporary service used for initialization
ansible.builtin.systemd:
name: k3s-init
@@ -181,24 +188,24 @@
- name: Create directory .kube
ansible.builtin.file:
path: "{{ ansible_user_dir }}/.kube"
path: "{{ ansible_facts['user_dir'] }}/.kube"
state: directory
owner: "{{ ansible_user_id }}"
owner: "{{ ansible_facts['user_id'] }}"
mode: u=rwx,g=rx,o=
- name: Copy config file to user home directory
ansible.builtin.copy:
src: /etc/rancher/k3s/k3s.yaml
dest: "{{ ansible_user_dir }}/.kube/config"
dest: "{{ ansible_facts['user_dir'] }}/.kube/config"
remote_src: true
owner: "{{ ansible_user_id }}"
owner: "{{ ansible_facts['user_id'] }}"
mode: u=rw,g=,o=
- name: Configure kubectl cluster to {{ endpoint_url }}
ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} config set-cluster default
--server={{ endpoint_url }}
--kubeconfig {{ ansible_user_dir }}/.kube/config
--kubeconfig {{ ansible_facts['user_dir'] }}/.kube/config
changed_when: true
vars:
endpoint_url: >-
+12 -6
View File
@@ -6,7 +6,8 @@
owner: root
group: root
mode: "0644"
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
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 }}"
ansible.builtin.get_url:
@@ -19,7 +20,8 @@
retries: "{{ download_retries }}"
delay: "{{ download_delay }}"
until: metallb_manifest_download is succeeded
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
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 }}
ansible.builtin.replace:
@@ -31,13 +33,15 @@
to: metallb/speaker:{{ metal_lb_speaker_tag_version }}
loop_control:
label: "{{ item.change }} => {{ item.to }}"
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Read back MetalLB manifest from first master
ansible.builtin.slurp:
src: /var/lib/rancher/k3s/server/manifests/metallb-crds.yaml
register: metallb_manifest
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Check the MetalLB speaker image reference is present
ansible.builtin.set_fact:
@@ -46,7 +50,8 @@
('quay.io/metallb/speaker:' ~ metal_lb_speaker_tag_version) in
(metallb_manifest.content | default('') | b64decode)
}}
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
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
ansible.builtin.fail:
@@ -56,5 +61,6 @@
was not found in the downloaded manifest. The upstream image
reference may have changed.
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
+6 -3
View File
@@ -10,7 +10,8 @@
owner: root
group: root
mode: "0644"
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Copy kube-vip RBAC manifest to first master
ansible.builtin.template:
@@ -19,7 +20,8 @@
owner: root
group: root
mode: "0644"
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- name: Copy vip manifest to first master
ansible.builtin.template:
@@ -28,4 +30,5 @@
owner: root
group: root
mode: "0644"
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
+1
View File
@@ -39,5 +39,6 @@ group_name_master: master
metal_lb_mode: layer2
metal_lb_available_timeout: 240s
metal_lb_controller_tag_version: v0.16.0
metal_lb_enabled: true
metal_lb_interfaces: []
metal_lb_ip_range: 192.168.30.80-192.168.30.90
+7
View File
@@ -133,6 +133,13 @@ argument_specs:
description: IP range for kube-vip load balancer
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:
description: Wait for MetalLB resources
default: 240s
+2 -1
View File
@@ -1,6 +1,7 @@
---
- name: Deploy Calico to cluster
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
run_once: true
block:
- name: Create manifests directory on first master
+2 -2
View File
@@ -184,7 +184,7 @@
--helm-set loadBalancer.mode={{ bpf_lb_mode }}
{% endif %}
environment:
KUBECONFIG: "{{ ansible_user_dir }}/.kube/config"
KUBECONFIG: "{{ ansible_facts['user_dir'] }}/.kube/config"
register: cilium_install_result
changed_when: cilium_install_result.rc == 0
# 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
ansible.builtin.command: cilium status --wait
environment:
KUBECONFIG: "{{ ansible_user_dir }}/.kube/config"
KUBECONFIG: "{{ ansible_facts['user_dir'] }}/.kube/config"
register: cilium_status
changed_when: false
until: cilium_status.rc == 0
+6 -5
View File
@@ -12,11 +12,12 @@
- name: Deploy metallb pool
ansible.builtin.include_tasks: metallb.yml
tags: metallb
# Deploy MetalLB unless kube-vip owns the load balancer IP range, or Cilium
# BGP is enabled (Cilium then provides its own load balancing). The cilium_bgp
# default keeps this safe when Cilium variables are not in scope at all (#644)
# while still deploying MetalLB when a non-BGP Cilium CNI is in use.
when: kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool)
# Deploy MetalLB unless explicitly disabled, kube-vip owns the load balancer
# IP range, or Cilium BGP is enabled (Cilium then provides its own load
# balancing). The cilium_bgp default keeps this safe when Cilium variables are
# not in scope at all (#644) while still deploying MetalLB when a non-BGP
# Cilium CNI is in use.
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
ansible.builtin.file:
+2 -2
View File
@@ -3,7 +3,7 @@
ansible.builtin.file:
path: /tmp/k3s
state: directory
owner: "{{ ansible_user_id }}"
owner: "{{ ansible_facts['user_id'] }}"
mode: "0755"
with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true
@@ -33,7 +33,7 @@
ansible.builtin.template:
src: metallb.crs.j2
dest: /tmp/k3s/metallb-crs.yaml
owner: "{{ ansible_user_id }}"
owner: "{{ ansible_facts['user_id'] }}"
mode: "0755"
with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true
+17 -9
View File
@@ -29,7 +29,7 @@
- name: Set SELinux to disabled state
ansible.posix.selinux:
state: disabled
when: ansible_os_family == "RedHat"
when: ansible_facts.os_family == "RedHat"
- name: Enable IPv4 forwarding
ansible.posix.sysctl:
@@ -63,31 +63,39 @@
executable: /bin/bash
register: br_netfilter_exists
changed_when: false
when: ansible_os_family == "RedHat"
when: ansible_facts.os_family == "RedHat"
tags: sysctl
- name: Install kernel-modules-extra if br_netfilter missing
ansible.builtin.yum:
name: kernel-modules-extra
name: "kernel-modules-extra-{{ ansible_kernel }}"
state: present
update_cache: true
register: kernel_modules_installed
when:
- ansible_os_family == "RedHat"
- ansible_facts.os_family == "RedHat"
- 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/
ansible.builtin.copy:
content: br_netfilter
dest: /etc/modules-load.d/br_netfilter.conf
mode: u=rw,g=,o=
when: ansible_os_family == "RedHat"
when: ansible_facts.os_family == "RedHat"
- name: Load br_netfilter
community.general.modprobe:
name: br_netfilter
state: present
when: ansible_os_family == "RedHat"
when: ansible_facts.os_family == "RedHat"
- name: Set bridge-nf-call-iptables (just to be sure)
ansible.posix.sysctl:
@@ -95,7 +103,7 @@
value: "1"
state: present
reload: true
when: ansible_os_family == "RedHat"
when: ansible_facts.os_family == "RedHat"
loop:
- net.bridge.bridge-nf-call-iptables
- net.bridge.bridge-nf-call-ip6tables
@@ -103,10 +111,10 @@
- name: Add /usr/local/bin to sudo secure_path
ansible.builtin.lineinfile:
line: Defaults secure_path = {{ secure_path[ansible_os_family] }}
line: Defaults secure_path = {{ secure_path[ansible_facts.os_family] }}
regexp: Defaults(\s)*secure_path(\s)*=
state: present
insertafter: EOF
path: /etc/sudoers
validate: visudo -cf %s
when: ansible_os_family in [ "RedHat", "Suse" ]
when: ansible_facts.os_family in [ "RedHat", "Suse" ]
+10 -3
View File
@@ -1,20 +1,27 @@
---
# 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
ansible.builtin.command: grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo
register: grep_cpuinfo_raspberrypi
failed_when: false
changed_when: false
check_mode: false
- name: Test for raspberry pi /proc/device-tree/model
ansible.builtin.command: grep -E "Raspberry Pi" /proc/device-tree/model
register: grep_device_tree_model_raspberrypi
failed_when: false
changed_when: false
check_mode: false
- name: Set raspberry_pi fact to true
ansible.builtin.set_fact:
raspberry_pi: true
when: grep_cpuinfo_raspberrypi.rc == 0 or grep_device_tree_model_raspberrypi.rc == 0
when: (grep_cpuinfo_raspberrypi.rc | default(1)) == 0 or (grep_device_tree_model_raspberrypi.rc | default(1)) == 0
- name: Set detected_distribution to Raspbian (ARM64 on Raspbian, Debian Buster/Bullseye/Bookworm)
ansible.builtin.set_fact:
@@ -49,8 +56,8 @@
with_first_found:
- "{{ action_ }}/{{ detected_distribution }}-{{ detected_distribution_major_version }}.yml"
- "{{ action_ }}/{{ detected_distribution }}.yml"
- "{{ action_ }}/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml"
- "{{ action_ }}/{{ ansible_distribution }}.yml"
- "{{ action_ }}/{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
- "{{ action_ }}/{{ ansible_facts['distribution'] }}.yml"
- "{{ action_ }}/default.yml"
vars:
action_: >-
+8 -4
View File
@@ -19,7 +19,7 @@
cluster_hostnames: >-
{{
groups['k3s_cluster']
| map('extract', hostvars, 'ansible_hostname')
| map('extract', hostvars, ['ansible_facts', 'hostname'])
| list
}}
run_once: true
@@ -77,9 +77,13 @@
hosts: master
environment: "{{ proxy_env | default({}) }}"
tasks:
- name: Copying kubeconfig from {{ hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname'] }}
- name: >-
Copying kubeconfig from
{{ hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname'] }}
ansible.builtin.fetch:
src: "{{ ansible_user_dir }}/.kube/config"
src: "{{ ansible_facts['user_dir'] }}/.kube/config"
dest: ./kubeconfig
flat: true
when: ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname']
when: >-
ansible_facts.hostname ==
hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']