Compare commits

...
Author SHA1 Message Date
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
Techno Tim 4dc333a7d3 fix(metallb): verify the metallb-system namespace actually exists (#702)
* 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

* fix(metallb): retry the namespace check on transient kube API errors

- retry the explicit get namespace check until it succeeds
- extend the regression test to assert the retry wiring

* fix(metallb): retry the webhook endpoint check on transient kube API errors

- retry the webhook-service endpoint get until it succeeds
- extend the regression test to cover the webhook task too
2026-08-07 22:36:02 -05:00
Techno Tim 287d8b7a27 feat(kube-vip): add endpoint override for the internal listening address (#699)
* feat(kube-vip): add endpoint override for the internal listening address

Add a kube_vip_endpoint variable so the address kube-vip binds and listens on
can differ from the announced apiserver_endpoint. This is useful for complex
routing and site-to-site tunnels where the VIP kube-vip advertises over ARP
differs from the address it listens on internally.

- roles/k3s_server/templates/vip.yaml.j2: use kube_vip_endpoint (defaulting to
  apiserver_endpoint) for the `address` env and for deriving `vip_subnet`
- roles/k3s_server/defaults/main.yml: add kube_vip_endpoint default (null)
- roles/k3s_server/meta/main.yml: add kube_vip_endpoint argument_spec
- inventory/sample/group_vars/all.yml: document the new sample variable
- README.md: document the kube_vip_endpoint option
- .github/scripts/test-kube-vip-manifest.py: extend regression test to cover the
  default (apiserver_endpoint) and the override case

Closes #221

* chore(ci): extend molecule job timeout to 3 hours

The default scenario occasionally takes longer than 150 minutes on the shared
nested-virt runner (k3s agent notify-wait can exceed the limit under load), and
a single timeout aborts the whole run before the other four scenarios execute.
Raise timeout-minutes from 150 to 180 so a slow-but-progressing run completes
instead of aborting.

The default scenario remains first in the matrix so a failure surfaces fastest.

* fix(kube-vip): fall back on null kube_vip_endpoint and cover it in the test

- vip_subnet and address use default(apiserver_endpoint, true) so the null
  role default falls back to the apiserver endpoint instead of rendering an
  empty/invalid address and subnet
- change the manifest regression test default case to pass kube_vip_endpoint
  as None so it pins the real runtime null condition and fails fast on this
  regression rather than timing out in CI
2026-08-06 02:48:57 -05:00
Techno Tim 56bb912bd3 feat(prereq): add toggle to disable swap on all cluster nodes (#698)
k3s recommends swap be disabled on every node. Add a disable_swap toggle
(default true) to the prereq role that turns swap off now and comments out the
swap entries in /etc/fstab so swap stays off across reboots. The change is
applied uniformly across all k3s_cluster hosts (all-or-nothing) since leaving
swap on for some nodes but not others creates uneven scheduling and latency.

- roles/prereq/defaults/main.yml: add disable_swap: true default
- roles/prereq/tasks/main.yml: idempotent swapoff -a and fstab comment-out block
  gated on disable_swap
- inventory/sample/group_vars/all.yml: document the new sample variable
- README.md: document the disable_swap option
- .github/scripts/test-disable-swap.sh: regression test
- .pre-commit-config.yaml: wire the test into pre-commit

Closes #670
2026-08-05 07:39:00 -05:00
24 changed files with 615 additions and 29 deletions

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repo_root="$(git rev-parse --show-toplevel)"
prereq_defaults="$repo_root/roles/prereq/defaults/main.yml"
prereq_tasks="$repo_root/roles/prereq/tasks/main.yml"
# #670: k3s recommends swap be disabled on all nodes. The prereq role must expose
# a disable_swap toggle (defaulting to true) that turns swap off now and comments
# out the /etc/fstab swap entries so swap stays off across reboots.
grep -Eq -- '^disable_swap: true' "$prereq_defaults" || {
printf 'prereq defaults are missing disable_swap: true\n' >&2
exit 1
}
grep -Fq -- 'Disable swap on all cluster nodes' "$prereq_tasks" || {
printf 'prereq tasks are missing the swap-disable block\n' >&2
exit 1
}
grep -Fq -- 'swapoff -a' "$prereq_tasks" || {
printf 'swap-disable block does not run swapoff -a\n' >&2
exit 1
}
grep -Fq -- '/etc/fstab' "$prereq_tasks" || {
printf 'swap-disable block does not comment out /etc/fstab swap entries\n' >&2
exit 1
}
if ! grep -Eq -- 'when: disable_swap' "$prereq_tasks"; then
printf 'swap-disable block is not gated on the disable_swap toggle\n' >&2
exit 1
fi
printf 'Swap disable regression test passed\n'
@@ -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()
+36 -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(
@@ -123,6 +123,38 @@ def main():
if "name: bgp_peers" in output:
fail("bgp_peers present even though the peer list is empty")
# kube_vip_endpoint defaults to null (defined in role defaults): the
# address and subnet must fall back to the apiserver endpoint. default()
# without a truthy flag does NOT fall back on null, only on undefined, so
# this case pins the null runtime condition to prevent that regression.
output = render(
env,
{
"_kube_vip_bgp_peers": [],
"kube_vip_endpoint": None,
"kube_vip_arp": True,
"kube_vip_bgp": False,
},
)
if "value: 192.168.30.222" not in output:
fail("null kube_vip_endpoint does not fall back to apiserver_endpoint")
# kube_vip_endpoint set: overrides the internal listening address AND the
# subnet derivation while the advertised apiserver_endpoint stays separate.
output = render(
env,
{
"_kube_vip_bgp_peers": [],
"kube_vip_endpoint": "10.66.1.5",
"kube_vip_arp": True,
"kube_vip_bgp": False,
},
)
if "value: 10.66.1.5" not in output:
fail("kube_vip_endpoint did not override the address")
if "value: 192.168.30.222" in output:
fail("apiserver_endpoint leaked into address when kube_vip_endpoint set")
print("kube-vip manifest regression test passed")
@@ -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():
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Regression test for the MetalLB converge checks.
The MetalLB tasks in roles/k3s_server_post/tasks/metallb.yml must actually
verify resources through an explicit kubectl get, and must retry on a
transient kube API error while MetalLB converges.
The "Test metallb-system namespace" task previously ran `k3s kubectl -n
metallb-system` with no subcommand, which only printed a usage page and always
exited 0, so it always succeeded even when the namespace did not exist (issue
#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
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("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():
task_file = os.path.join(
repo_root(), "roles", "k3s_server_post", "tasks", "metallb.yml"
)
with open(task_file, encoding="utf-8") as handle:
tasks = yaml.safe_load(handle)
namespace_task = find_task(tasks, "Test metallb-system namespace")
# 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
# use an explicit get.
check_explicit_get(namespace_task, "Test metallb-system namespace",
"get namespace metallb-system")
check_retry_wiring(namespace_task, "Test metallb-system namespace")
webhook_task = find_task(tasks, "Test metallb-system webhook-service endpoint")
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")
if __name__ == "__main__":
main()
@@ -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@c5fc58bd0be7a4b94b73ce40250322d5b838a108 # 5.0.7
with:
allowlist: |
aws-actions/
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
trap stop_monitor EXIT
/usr/bin/time -v -o "$timing_file" \
molecule test --scenario-name ${{ matrix.scenario }}
timeout-minutes: 150
timeout-minutes: 180
env:
ANSIBLE_K3S_LOG_DIR: ${{ runner.temp }}/logs/k3s-ansible/${{ matrix.scenario }}
ANSIBLE_SSH_RETRIES: 4
+31
View File
@@ -68,6 +68,12 @@ repos:
language: system
pass_filenames: false
files: ^site\.yml$|^\.github/scripts/test-unique-hostname-precheck\.sh$
- id: disable-swap-test
name: Disable swap test
entry: .github/scripts/test-disable-swap.sh
language: system
pass_filenames: false
files: ^roles/prereq/(tasks/main|defaults/main)\.yml$|^\.github/scripts/test-disable-swap\.sh$
- id: cilium-bgp-manifest-test
name: Cilium BGP manifest test
entry: python3 .github/scripts/test-cilium-bgp-manifest.py
@@ -107,6 +113,14 @@ repos:
- Jinja2>=3.1
pass_filenames: false
files: ^roles/k3s_server_post/templates/metallb\.crs\.j2$|^\.github/scripts/test-metallb-interfaces\.py$
- id: metallb-namespace-test
name: MetalLB namespace test
entry: python3 .github/scripts/test-metallb-namespace.py
language: python
additional_dependencies:
- PyYAML
pass_filenames: false
files: ^roles/k3s_server_post/tasks/metallb\.yml$|^\.github/scripts/test-metallb-namespace\.py$
- id: metallb-deploy-condition-test
name: MetalLB deploy condition test
entry: python3 .github/scripts/test-metallb-deploy-condition.py
@@ -116,6 +130,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
@@ -124,3 +147,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]
+5 -1
View File
@@ -219,9 +219,12 @@ See the commands [here](https://technotim.com/posts/k3s-etcd-ansible/#testing-yo
| `k3s_server` | `kube_vip_bgp_peers` | list | `[]` | Not required | List of BGP peer ASN & address pairs |
| `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_tag_version` | string | `v1.2.2` | Not required | Image tag for kube-vip |
| `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_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` |
@@ -261,6 +264,7 @@ See the commands [here](https://technotim.com/posts/k3s-etcd-ansible/#testing-yo
| `reboot` (playbook) | `concurrent_reboots` | int/string | `100%` | Not required | Number (or percentage) of nodes to reboot at a time for a staggered reboot |
| `reboot` (playbook) | `wait_seconds_after_reboot` | int | `0` | Not required | Pause in seconds between staggered reboot batches |
| `prereq` | `system_timezone` | string | `null` | Not required | Timezone to be set on all nodes |
| `prereq` | `disable_swap` | bool | `true` | Not required | Disable swap on all cluster nodes (swapoff + comment out /etc/fstab swap entries), all-or-nothing |
| `proxmox_lxc`, `reset_proxmox_lxc` | `proxmox_lxc_ct_ids` | list | ❌ | Required | Proxmox container ID list |
| `raspberrypi` | `state` | string | `present` | Not required | Indicates whether the k3s prerequisites for Raspberry Pi should be set up (possible values are `present` and `absent`) |
+19 -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
@@ -7,6 +7,10 @@ systemd_dir: /etc/systemd/system
# Set your timezone
system_timezone: Your/Timezone
# k3s recommends swap be disabled on every cluster node. Applied uniformly to all
# nodes (all-or-nothing) in the prereq role. Set to false to leave swap enabled.
disable_swap: true
# interface which will be used for flannel
# Defaults to each host's default IPv4 interface (e.g. eth0, enp1s0, ens3)
# so KVM/cloud hosts without eth0 work out of the box. Override per-host if needed.
@@ -44,6 +48,11 @@ cilium_bgp_lb_cidr: 192.168.31.0/24 # cidr for cilium loadbalancer ipam
# enable kube-vip ARP broadcasts
kube_vip_arp: true
# (optional) overrides the address kube-vip binds/listens on internally, which
# can differ from the announced apiserver_endpoint for complex routing/tunnels.
# Defaults to apiserver_endpoint. Also used to derive the kube-vip subnet.
# kube_vip_endpoint: 10.66.1.5
# enable kube-vip BGP peering
kube_vip_bgp: false
@@ -93,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"
@@ -102,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 }}
+1 -1
View File
@@ -1,4 +1,4 @@
ansible-core>=2.19.11
ansible-core>=2.19.12
jmespath>=1.1.0
jsonpatch>=1.33
kubernetes>=29.0.0
+1 -1
View File
@@ -6,7 +6,7 @@
#
ansible-compat==4.1.11
# via molecule
ansible-core==2.19.11
ansible-core==2.19.12
# via
# -r requirements.in
# ansible-compat
+5 -1
View File
@@ -7,8 +7,9 @@ group_name_master: master
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
@@ -19,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
+25 -1
View File
@@ -78,18 +78,42 @@ argument_specs:
- automatically at runtime.
default: ~
kube_vip_endpoint:
description:
- Overrides the address kube-vip binds/listens on internally, which
- can differ from the announced apiserver_endpoint for complex
- routing and site-to-site tunnels.
- Defaults to apiserver_endpoint and is used to derive the kube-vip
- subnet.
default: ~
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
+8 -6
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
+2 -2
View File
@@ -37,7 +37,7 @@ spec:
value: {{ kube_vip_iface }}
{% endif %}
- name: vip_subnet
value: "{{ apiserver_endpoint | ansible.utils.ipsubnet | ansible.utils.ipaddr('prefix') }}"
value: "{{ (kube_vip_endpoint | default(apiserver_endpoint, true)) | ansible.utils.ipsubnet | ansible.utils.ipaddr('prefix') }}"
- name: cp_enable
value: "true"
- name: cp_namespace
@@ -55,7 +55,7 @@ spec:
- name: vip_retryperiod
value: "2"
- name: address
value: {{ apiserver_endpoint }}
value: {{ kube_vip_endpoint | default(apiserver_endpoint, true) }}
{% if kube_vip_bgp | default(false) | bool %}
{% if kube_vip_bgp_routerid is defined %}
- name: bgp_routerid
+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
+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:
+13 -1
View File
@@ -40,8 +40,14 @@
- name: Test metallb-system namespace
ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} -n metallb-system
{{ k3s_kubectl_binary | default('k3s kubectl') }} get namespace metallb-system
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')] }}"
run_once: true
@@ -99,6 +105,12 @@
ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} -n metallb-system get endpoints {{ metallb_webhook_service_name }}
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')] }}"
run_once: true
+2
View File
@@ -1,4 +1,6 @@
---
disable_swap: true
secure_path:
RedHat: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
Suse: /usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin
+22
View File
@@ -4,6 +4,28 @@
name: "{{ system_timezone }}"
when: (system_timezone is defined) and (system_timezone != "Your/Timezone")
# k3s recommends swap be disabled on all nodes. Disabling swap is all-or-nothing
# across the cluster: leaving it enabled on some nodes but not others creates
# uneven scheduling/latency behavior. This block turns swap off and comments out
# the swap entries in /etc/fstab so it stays off across reboots. It is idempotent
# and a no-op when swap is already disabled or swapoff is unavailable.
- name: Disable swap on all cluster nodes
when: disable_swap
block:
- name: Turn off swap now
ansible.builtin.command: swapoff -a
register: swapoff_result
changed_when: false
failed_when: false
- name: Comment out swap entries in fstab
ansible.builtin.replace:
path: /etc/fstab
regexp: '^([^#][^\n]*\s+swap\s+)'
replace: '# \\1'
register: fstab_swap
- name: Set SELinux to disabled state
ansible.posix.selinux:
state: disabled