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
This commit is contained in:
Timothy Stewart
2026-08-14 12:34:23 -05:00
committed by Techno Tim
parent 4dc333a7d3
commit 1687a3b098
11 changed files with 241 additions and 12 deletions
@@ -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()
@@ -10,7 +10,8 @@ scenario.
This test loads the real `when` expressions from both task files and evaluates This test loads the real `when` expressions from both task files and evaluates
them against representative variable sets, asserting MetalLB is deployed in them against representative variable sets, asserting MetalLB is deployed in
every topology except when 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 from __future__ import print_function
@@ -23,6 +24,7 @@ import yaml
from jinja2 import Environment from jinja2 import Environment
METALLB_WHEN = ( METALLB_WHEN = (
"metal_lb_enabled and "
"kube_vip_lb_ip_range is not defined and " "kube_vip_lb_ip_range is not defined and "
"not (cilium_bgp | default(false) | bool)" "not (cilium_bgp | default(false) | bool)"
) )
@@ -88,6 +90,7 @@ def scenarios():
yield ( yield (
# Default Flannel inventory (all.yml sets cilium_bgp: false). # Default Flannel inventory (all.yml sets cilium_bgp: false).
{ {
"metal_lb_enabled": True,
"cilium_bgp": False, "cilium_bgp": False,
"cilium_iface": None, "cilium_iface": None,
}, },
@@ -98,6 +101,7 @@ def scenarios():
# Calico CNI with no Cilium variable in scope (issue #644): cilium_bgp # Calico CNI with no Cilium variable in scope (issue #644): cilium_bgp
# is genuinely undefined, so `default(false)` must keep MetalLB on. # is genuinely undefined, so `default(false)` must keep MetalLB on.
{ {
"metal_lb_enabled": True,
"calico_iface": "eth1", "calico_iface": "eth1",
}, },
True, True,
@@ -106,6 +110,7 @@ def scenarios():
yield ( yield (
# Cilium CNI with BGP disabled: MetalLB must still be deployed. # Cilium CNI with BGP disabled: MetalLB must still be deployed.
{ {
"metal_lb_enabled": True,
"cilium_bgp": False, "cilium_bgp": False,
"cilium_iface": "eth1", "cilium_iface": "eth1",
}, },
@@ -115,6 +120,7 @@ def scenarios():
yield ( yield (
# Cilium CNI with BGP enabled: Cilium provides the LB, skip MetalLB. # Cilium CNI with BGP enabled: Cilium provides the LB, skip MetalLB.
{ {
"metal_lb_enabled": True,
"cilium_bgp": True, "cilium_bgp": True,
"cilium_iface": "eth1", "cilium_iface": "eth1",
}, },
@@ -124,12 +130,21 @@ def scenarios():
yield ( yield (
# kube-vip is the load balancer provider: skip MetalLB. # kube-vip is the load balancer provider: skip MetalLB.
{ {
"metal_lb_enabled": True,
"kube_vip_lb_ip_range": "192.168.30.80-192.168.30.90", "kube_vip_lb_ip_range": "192.168.30.80-192.168.30.90",
"cilium_bgp": False, "cilium_bgp": False,
}, },
False, False,
"kube-vip owns the VIP range", "kube-vip owns the VIP range",
) )
yield (
# MetalLB explicitly disabled (external LB / single node): skip.
{
"metal_lb_enabled": False,
},
False,
"metal_lb_enabled: false (external LB)",
)
def main(): def main():
+9
View File
@@ -130,6 +130,15 @@ repos:
- PyYAML - PyYAML
pass_filenames: false pass_filenames: false
files: ^roles/k3s_server/tasks/main\.yml$|^roles/k3s_server_post/tasks/main\.yml$|^\.github/scripts/test-metallb-deploy-condition\.py$ # noqa yaml[line-length] files: ^roles/k3s_server/tasks/main\.yml$|^roles/k3s_server_post/tasks/main\.yml$|^\.github/scripts/test-metallb-deploy-condition\.py$ # noqa yaml[line-length]
- id: kube-vip-deploy-condition-test
name: kube-vip deploy condition test
entry: python3 .github/scripts/test-kube-vip-deploy-condition.py
language: python
additional_dependencies:
- Jinja2>=3.1
- PyYAML
pass_filenames: false
files: ^roles/k3s_server/tasks/main\.yml$|^\.github/scripts/test-kube-vip-deploy-condition\.py$ # noqa yaml[line-length]
- id: default-interface-test - id: default-interface-test
name: default interface test name: default interface test
entry: python3 .github/scripts/test-default-interface.py entry: python3 .github/scripts/test-default-interface.py
+2
View File
@@ -222,7 +222,9 @@ See the commands [here](https://technotim.com/posts/k3s-etcd-ansible/#testing-yo
| `k3s_server` | `kube_vip_endpoint` | string | `~` | Not required | Overrides the internal address kube-vip binds/listens on, which can differ from the announced apiserver_endpoint for complex routing/tunnels. Defaults to apiserver_endpoint. | | `k3s_server` | `kube_vip_endpoint` | string | `~` | Not required | Overrides the internal address kube-vip binds/listens on, which can differ from the announced apiserver_endpoint for complex routing/tunnels. Defaults to apiserver_endpoint. |
| `k3s_server` | `kube_vip_tag_version` | string | `v1.2.2` | Not required | Image tag for kube-vip | | `k3s_server` | `kube_vip_tag_version` | string | `v1.2.2` | Not required | Image tag for kube-vip |
| `k3s_server` | `kube_vip_cloud_provider_tag_version` | string | `v0.0.12` | Not required | Tag for kube-vip-cloud-provider manifest when enable | | `k3s_server` | `kube_vip_cloud_provider_tag_version` | string | `v0.0.12` | Not required | Tag for kube-vip-cloud-provider manifest when enable |
| `k3s_server` | `kube_vip_enabled` | bool | `true` | Not required | Enable kube-vip install, covering both the control-plane VIP and the service load balancer. Set false to skip kube-vip entirely (single node or external LB). |
| `k3s_server`, `k3_server_post` | `kube_vip_lb_ip_range` | string | `~` | Not required | IP range for kube-vip load balancer | | `k3s_server`, `k3_server_post` | `kube_vip_lb_ip_range` | string | `~` | Not required | IP range for kube-vip load balancer |
| `k3s_server`, `k3s_server_post` | `metal_lb_enabled` | bool | `true` | Not required | Enable MetalLB install for service load balancing. Set false to skip MetalLB (external LB). |
| `k3s_server`, `k3s_server_post` | `metal_lb_controller_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB | | `k3s_server`, `k3s_server_post` | `metal_lb_controller_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB |
| `k3s_server` | `metal_lb_speaker_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB | | `k3s_server` | `metal_lb_speaker_tag_version` | string | `v0.16.0` | Not required | Image tag for MetalLB |
| `k3s_server` | `metal_lb_type` | string | `native` | Not required | Use FRR mode or native. Valid values are `frr` and `native` | | `k3s_server` | `metal_lb_type` | string | `native` | Not required | Use FRR mode or native. Valid values are `frr` and `native` |
+8
View File
@@ -104,6 +104,10 @@ extra_agent_args: >-
# image tag for kube-vip # image tag for kube-vip
kube_vip_tag_version: v1.2.2 kube_vip_tag_version: v1.2.2
# enable kube-vip (covers both the control-plane VIP and service load balancing)
# set false for a single node or when an external load balancer is used
kube_vip_enabled: true
# tag for kube-vip-cloud-provider manifest # tag for kube-vip-cloud-provider manifest
# kube_vip_cloud_provider_tag_version: "v0.0.12" # kube_vip_cloud_provider_tag_version: "v0.0.12"
@@ -111,6 +115,10 @@ kube_vip_tag_version: v1.2.2
# (uncomment to use kube-vip for services instead of MetalLB) # (uncomment to use kube-vip for services instead of MetalLB)
# kube_vip_lb_ip_range: "192.168.30.80-192.168.30.90" # kube_vip_lb_ip_range: "192.168.30.80-192.168.30.90"
# enable MetalLB for service load balancing
# set false when using an external load balancer
metal_lb_enabled: true
# metallb type frr or native # metallb type frr or native
metal_lb_type: native metal_lb_type: native
+3
View File
@@ -20,6 +20,9 @@ kube_vip_bgp_peeras: "64512"
kube_vip_bgp_peers: [] kube_vip_bgp_peers: []
kube_vip_bgp_peers_groups: ['k3s_master'] kube_vip_bgp_peers_groups: ['k3s_master']
kube_vip_enabled: true
metal_lb_enabled: true
metal_lb_controller_tag_version: v0.16.0 metal_lb_controller_tag_version: v0.16.0
metal_lb_speaker_tag_version: v0.16.0 metal_lb_speaker_tag_version: v0.16.0
metal_lb_type: native metal_lb_type: native
+15
View File
@@ -95,10 +95,25 @@ argument_specs:
description: Tag for kube-vip-cloud-provider manifest when enabled description: Tag for kube-vip-cloud-provider manifest when enabled
default: v0.0.12 default: v0.0.12
kube_vip_enabled:
description:
- Enable installing kube-vip.
- Covers both the control-plane VIP and the service load balancer.
- Set false to skip kube-vip entirely, for example on a single node or when an external load balancer is used.
default: true
type: bool
kube_vip_lb_ip_range: kube_vip_lb_ip_range:
description: IP range for kube-vip load balancer description: IP range for kube-vip load balancer
default: ~ default: ~
metal_lb_enabled:
description:
- Enable installing MetalLB for service load balancing.
- Set false to skip MetalLB, for example when an external load balancer is used.
default: true
type: bool
metal_lb_controller_tag_version: metal_lb_controller_tag_version:
description: Image tag for MetalLB description: Image tag for MetalLB
default: v0.16.0 default: v0.16.0
+8 -6
View File
@@ -24,19 +24,21 @@
- name: Deploy vip manifest - name: Deploy vip manifest
ansible.builtin.include_tasks: vip.yml ansible.builtin.include_tasks: vip.yml
when: kube_vip_enabled
- name: Deploy metallb manifest - name: Deploy metallb manifest
ansible.builtin.include_tasks: metallb.yml ansible.builtin.include_tasks: metallb.yml
tags: metallb tags: metallb
# Deploy MetalLB unless kube-vip owns the load balancer IP range, or Cilium # Deploy MetalLB unless explicitly disabled, kube-vip owns the load balancer
# BGP is enabled (Cilium then provides its own load balancing). The cilium_bgp # IP range, or Cilium BGP is enabled (Cilium then provides its own load
# default keeps this safe when Cilium variables are not in scope at all (#644) # balancing). The cilium_bgp default keeps this safe when Cilium variables are
# while still deploying MetalLB when a non-BGP Cilium CNI is in use. # not in scope at all (#644) while still deploying MetalLB when a non-BGP
when: kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool) # 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 - name: Deploy kube-vip manifest
ansible.builtin.include_tasks: kube-vip.yml ansible.builtin.include_tasks: kube-vip.yml
tags: kubevip tags: kubevip
when: kube_vip_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 - name: Initialize and verify the K3s control plane
any_errors_fatal: true any_errors_fatal: true
+1
View File
@@ -39,5 +39,6 @@ group_name_master: master
metal_lb_mode: layer2 metal_lb_mode: layer2
metal_lb_available_timeout: 240s metal_lb_available_timeout: 240s
metal_lb_controller_tag_version: v0.16.0 metal_lb_controller_tag_version: v0.16.0
metal_lb_enabled: true
metal_lb_interfaces: [] metal_lb_interfaces: []
metal_lb_ip_range: 192.168.30.80-192.168.30.90 metal_lb_ip_range: 192.168.30.80-192.168.30.90
+7
View File
@@ -133,6 +133,13 @@ argument_specs:
description: IP range for kube-vip load balancer description: IP range for kube-vip load balancer
default: ~ default: ~
metal_lb_enabled:
description:
- Enable installing the MetalLB pool for service load balancing.
- Set false to skip MetalLB, for example when an external load balancer is used.
default: true
type: bool
metal_lb_available_timeout: metal_lb_available_timeout:
description: Wait for MetalLB resources description: Wait for MetalLB resources
default: 240s default: 240s
+6 -5
View File
@@ -12,11 +12,12 @@
- name: Deploy metallb pool - name: Deploy metallb pool
ansible.builtin.include_tasks: metallb.yml ansible.builtin.include_tasks: metallb.yml
tags: metallb tags: metallb
# Deploy MetalLB unless kube-vip owns the load balancer IP range, or Cilium # Deploy MetalLB unless explicitly disabled, kube-vip owns the load balancer
# BGP is enabled (Cilium then provides its own load balancing). The cilium_bgp # IP range, or Cilium BGP is enabled (Cilium then provides its own load
# default keeps this safe when Cilium variables are not in scope at all (#644) # balancing). The cilium_bgp default keeps this safe when Cilium variables are
# while still deploying MetalLB when a non-BGP Cilium CNI is in use. # not in scope at all (#644) while still deploying MetalLB when a non-BGP
when: kube_vip_lb_ip_range is not defined and not (cilium_bgp | default(false) | bool) # 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 - name: Remove tmp directory used for manifests
ansible.builtin.file: ansible.builtin.file: