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
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
Techno Tim cf76292169 feat(site): fail fast when cluster nodes share a hostname (#697)
Add a preflight assertion to site.yml that confirms every host in the
k3s_cluster group reports a unique hostname. k3s registers each node keyed by
its hostname, so duplicate hostnames prevent nodes from joining the cluster
and are hard to troubleshoot. The check fails fast in the Pre tasks instead of
surfacing as a cryptic registration failure later.

- site.yml: assert (cluster_hostnames | unique | length) == length for the
  k3s_cluster group, skipping hosts outside that group
- README.md: document the unique-hostname requirement
- .github/scripts/test-unique-hostname-precheck.sh: regression test
- .pre-commit-config.yaml: wire the test into pre-commit

Closes #636
2026-08-05 05:14:29 -05:00
Techno TimandLeo 52c086d638 feat(metallb): add option to limit MetalLB layer2 announcements to specific interfaces (#696)
- roles/k3s_server_post/templates/metallb.crs.j2: render spec.interfaces in the
  L2Advertisement when metal_lb_interfaces is a non-empty list, so MetalLB only
  announces on the configured interfaces. Empty list (default) keeps announcing
  on all interfaces, preserving existing behavior.
- roles/k3s_server_post/defaults/main.yml: add metal_lb_interfaces: [] default
- roles/k3s_server_post/meta/main.yml: add metal_lb_interfaces argument_spec
- inventory/sample/group_vars/all.yml: document the new sample variable
- .github/scripts/test-metallb-interfaces.py: regression test rendering the
  template for empty/single/multiple interfaces and confirming the BGP path is
  unaffected
- .pre-commit-config.yaml: wire the new test into pre-commit

Co-authored-by: Leo <leo@kuboschek.me>
2026-08-05 02:48:24 -05:00
Techno TimandLéo Nonnenmacher 010551b8d2 feat(cilium): add toggle to enable or disable the Envoy proxy (#695)
Add a `cilium_envoy` variable (default true, matching upstream Cilium 1.20
which installs Envoy by default) that controls whether the Envoy proxy is
deployed for Cilium L7 policies. Pass it to Helm as `envoy.enabled` so users
with no L7 policies can skip Envoy to save resources.

- roles/k3s_server_post/defaults/main.yml: add cilium_envoy: true default
- roles/k3s_server_post/tasks/cilium.yml: add --helm-set envoy.enabled to the
  install/upgrade command, driven by the cilium_envoy conditional
- inventory/sample/group_vars/all.yml: document cilium_envoy sample var
- .github/scripts/test-cilium-envoy-toggle.py: regression test asserting the
  install command carries the envoy.enabled helm-set and renders true/false
- .pre-commit-config.yaml: wire the new test into pre-commit

Co-authored-by: Léo Nonnenmacher <leo@nonnenmacher-logel.fr>
2026-08-05 00:00:39 -05:00
40 changed files with 1210 additions and 86 deletions

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Regression test for the Cilium Envoy toggle.
The `cilium_envoy` variable lets users enable or disable the Cilium Envoy
proxy. The Install/upgrade Cilium task in
roles/k3s_server_post/tasks/cilium.yml passes the value through to Helm as
`envoy.enabled`. This test:
- loads the real "Install Cilium" task and confirms the install/upgrade
command actually contains the `envoy.enabled` Helm value,
- renders the conditional that computes the Helm value and confirms it
produces `true` when cilium_envoy is enabled and `false` when disabled,
- confirms the task stays forward/backward compatible (no raw `true` /
`false` hardcoded in place of the conditional).
"""
from __future__ import print_function
import os
import re
import subprocess
import yaml
from jinja2 import Environment
ENVOY_EXPRESSION = '{{ "true" if cilium_envoy else "false" }}'
def repo_root():
return subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
def fail(message):
raise SystemExit("Cilium Envoy toggle test failed: " + message)
def extract_install_command(path):
"""Return the command string for the 'Install Cilium' task.
Walks both top-level tasks and tasks nested inside a `block`/`always`/
`rescue` list, since the Cilium deploy steps are grouped under the
'Prepare Cilium CLI on first master and deploy CNI' block.
"""
with open(path, encoding="utf-8") as handle:
doc = yaml.safe_load(handle)
def find_command(tasks):
for task in tasks:
if not isinstance(task, dict):
continue
if task.get("name") == "Install Cilium":
command = task.get("ansible.builtin.command")
if command is None:
raise SystemExit(
"Cilium Envoy toggle test failed: "
"'Install Cilium' task has no ansible.builtin.command"
)
return command
# Recurse into block/always/rescue sub-lists.
for key in ("block", "always", "rescue"):
nested = task.get(key)
if isinstance(nested, list):
found = find_command(nested)
if found is not None:
return found
return None
command = find_command(doc)
if command is None:
raise SystemExit(
"Cilium Envoy toggle test failed: could not find 'Install Cilium' task"
)
return command
def assert_envoy_in_command(command):
if "envoy.enabled" not in command:
fail("install command is missing --helm-set envoy.enabled")
if ENVOY_EXPRESSION not in command:
fail(
"install command does not use the cilium_envoy conditional: "
"expected {0!r}".format(ENVOY_EXPRESSION)
)
# The conditional must be a WYSIWYG helm-set value, not a pre-rendered
# true/false literal (which would ignore the cilium_envoy variable).
if re.search(r"--helm-set envoy\.enabled=true(?:$|\s)", command):
fail("install command hardcodes envoy.enabled=true")
if re.search(r"--helm-set envoy\.enabled=false(?:$|\s)", command):
fail("install command hardcodes envoy.enabled=false")
def assert_render():
env = Environment()
def render_for(value):
template = env.from_string(ENVOY_EXPRESSION)
return template.render(cilium_envoy=value)
if render_for(True) != "true":
fail("envoy conditional did not render 'true' when enabled")
if render_for(False) != "false":
fail("envoy conditional did not render 'false' when disabled")
def main():
root = repo_root()
cilium_tasks = os.path.join(
root, "roles", "k3s_server_post", "tasks", "cilium.yml"
)
command = extract_install_command(cilium_tasks)
assert_envoy_in_command(command)
assert_render()
print("Cilium Envoy toggle regression test passed")
if __name__ == "__main__":
main()
+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'
+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 exit 1
fi 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 -- 'difference(nodes.stdout.split())' "$main_tasks"
grep -Fq -- 'crd/addons.k3s.cattle.io' "$main_tasks" grep -Fq -- 'crd/addons.k3s.cattle.io' "$main_tasks"
grep -Fq -- 'crd/helmcharts.helm.cattle.io' "$main_tasks" grep -Fq -- 'crd/helmcharts.helm.cattle.io' "$main_tasks"
@@ -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 #!/usr/bin/env python3
"""Render the kube-vip DaemonSet template and assert env key correctness. """Render the kube-vip DaemonSet template and assert env key correctness.
kube-vip v1.2.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 `bgppeers` and `vip_cidr` names. This test proves the rendered manifest uses
the keys the target image actually parses. the keys the target image actually parses.
""" """
@@ -74,7 +74,7 @@ def render(env, extra_vars):
{"peer_address": "192.168.30.1", "peer_asn": "64512"}, {"peer_address": "192.168.30.1", "peer_asn": "64512"},
{"peer_address": "192.168.30.2", "peer_asn": "64513"}, {"peer_address": "192.168.30.2", "peer_asn": "64513"},
], ],
"kube_vip_tag_version": "v1.2.2", "kube_vip_tag_version": "v1.2.3",
} }
base_vars.update(extra_vars) base_vars.update(extra_vars)
template = env.get_template("vip.yaml.j2") template = env.get_template("vip.yaml.j2")
@@ -105,8 +105,8 @@ def main():
fail("rendered manifest still uses the ignored vip_cidr key") fail("rendered manifest still uses the ignored vip_cidr key")
if "192.168.30.1:64512,192.168.30.2:64513" not in output: if "192.168.30.1:64512,192.168.30.2:64513" not in output:
fail("bgp_peers value is not comma-separated address:ASN entries") fail("bgp_peers value is not comma-separated address:ASN entries")
if "ghcr.io/kube-vip/kube-vip:v1.2.2" not in output: if "ghcr.io/kube-vip/kube-vip:v1.2.3" not in output:
fail("kube-vip image tag is not v1.2.2") fail("kube-vip image tag is not v1.2.3")
# BGP enabled with no merged peers: single-peer fallback vars, no bgp_peers. # BGP enabled with no merged peers: single-peer fallback vars, no bgp_peers.
output = render( output = render(
@@ -123,6 +123,38 @@ def main():
if "name: bgp_peers" in output: if "name: bgp_peers" in output:
fail("bgp_peers present even though the peer list is empty") 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") 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 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():
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Regression test for the MetalLB L2Advertisement interfaces.
`metal_lb_interfaces` restricts which network interfaces MetalLB announces
load balancer IPs on in layer2 mode. When the list is non-empty, the
L2Advertisement in roles/k3s_server_post/templates/metallb.crs.j2 must render
a `spec.interfaces` block; when it is empty (the default), no spec is rendered
so MetalLB announces on all interfaces.
This renders the template and asserts both cases plus the BGP path (which must
not be affected by the L2 interfaces variable).
"""
from __future__ import print_function
import os
import subprocess
from jinja2 import Environment, FileSystemLoader, StrictUndefined
def repo_root():
return subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
def fail(message):
raise SystemExit("MetalLB interfaces test failed: " + message)
def render(env, extra_vars):
base_vars = {
"metal_lb_mode": "layer2",
"metal_lb_ip_range": "192.168.30.80-192.168.30.90",
}
base_vars.update(extra_vars)
template = env.get_template("metallb.crs.j2")
return template.render(**base_vars)
def main():
root = repo_root()
template_dir = os.path.join(
root, "roles", "k3s_server_post", "templates"
)
env = Environment(
loader=FileSystemLoader(template_dir), undefined=StrictUndefined
)
# Empty list (default): no spec.interfaces in the L2Advertisement.
output = render(env, {"metal_lb_interfaces": []})
if "spec:\n interfaces:" in output:
fail("spec.interfaces rendered with an empty metal_lb_interfaces")
if "kind: L2Advertisement" not in output:
fail("L2Advertisement missing in layer2 mode")
# Single interface.
output = render(env, {"metal_lb_interfaces": ["eth1"]})
if "spec:\n interfaces:\n - eth1" not in output:
fail("single interface was not rendered in spec.interfaces")
# Multiple interfaces.
output = render(env, {"metal_lb_interfaces": ["eth1", "eth2"]})
if "spec:\n interfaces:\n - eth1\n - eth2" not in output:
fail("multiple interfaces were not rendered in spec.interfaces")
# BGP mode must not emit an L2Advertisement spec at all.
output = render(
env,
{
"metal_lb_mode": "bgp",
"metal_lb_interfaces": ["eth1"],
"metal_lb_bgp_my_asn": "64513",
"metal_lb_bgp_peer_asn": "64512",
"metal_lb_bgp_peer_address": "192.168.30.1",
},
)
if "kind: L2Advertisement" in output:
fail("L2Advertisement rendered in bgp mode")
if "interfaces:" in output:
fail("interfaces rendered in bgp mode")
print("MetalLB interfaces regression test passed")
if __name__ == "__main__":
main()
+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()
+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'
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repo_root="$(git rev-parse --show-toplevel)"
site_play="$repo_root/site.yml"
# #636: verify the "Pre tasks" play asserts that all k3s_cluster hosts have
# unique hostnames, so a duplicate-hostname inventory fails fast instead of
# silently breaking node registration/joining.
grep -Fq -- 'Verify all cluster nodes have unique hostnames' "$site_play" || {
printf 'site.yml is missing the unique-hostname preflight check\n' >&2
exit 1
}
# The check must deduplicate the cluster hostname list via the `unique` filter
# and compare lengths, i.e. groups['k3s_cluster'] must be referenced.
grep -Fq -- "groups['k3s_cluster']" "$site_play" || {
printf 'unique-hostname check does not iterate the k3s_cluster group\n' >&2
exit 1
}
if ! grep -Eq -- 'cluster_hostnames.*\|.*unique|\| unique' "$site_play"; then
printf 'unique-hostname check does not deduplicate the hostname list\n' >&2
exit 1
fi
printf 'Unique hostname preflight regression test passed\n'
@@ -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 - name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1
- name: Ensure SHA pinned actions - name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@46cfe808a5f1588656ef299eedd0ce2fd7ec0dcc # 5.0.6 uses: zgosalvez/github-actions-ensure-sha-pinned-actions@c4e71056006d29f90204b2d063be337109507d62 # 5.0.10
with: with:
allowlist: | allowlist: |
aws-actions/ aws-actions/
+2 -2
View File
@@ -88,7 +88,7 @@ jobs:
trap stop_monitor EXIT trap stop_monitor EXIT
/usr/bin/time -v -o "$timing_file" \ /usr/bin/time -v -o "$timing_file" \
molecule test --scenario-name ${{ matrix.scenario }} molecule test --scenario-name ${{ matrix.scenario }}
timeout-minutes: 150 timeout-minutes: 180
env: env:
ANSIBLE_K3S_LOG_DIR: ${{ runner.temp }}/logs/k3s-ansible/${{ matrix.scenario }} ANSIBLE_K3S_LOG_DIR: ${{ runner.temp }}/logs/k3s-ansible/${{ matrix.scenario }}
ANSIBLE_SSH_RETRIES: 4 ANSIBLE_SSH_RETRIES: 4
@@ -109,7 +109,7 @@ jobs:
- name: Upload log files - name: Upload log files
if: always() # do this even if a step before has failed 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: with:
name: logs-${{ matrix.scenario }}-${{ github.run_id }}-${{ github.run_attempt }} name: logs-${{ matrix.scenario }}-${{ github.run_id }}-${{ github.run_attempt }}
path: | path: |
+72 -4
View File
@@ -23,10 +23,6 @@ repos:
additional_dependencies: [ansible-core==2.18.0] additional_dependencies: [ansible-core==2.18.0]
language_version: python3.12 language_version: python3.12
args: [--offline] args: [--offline]
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4 rev: v1.5.4
hooks: hooks:
@@ -38,6 +34,12 @@ repos:
- id: fix-smartquotes - id: fix-smartquotes
- repo: local - repo: local
hooks: hooks:
- id: shellcheck
name: shellcheck
entry: shellcheck
language: python
additional_dependencies: [shellcheck-py==0.11.0.1]
types: [shell]
- id: cleanup-runner-resources-test - id: cleanup-runner-resources-test
name: cleanup runner resources test name: cleanup runner resources test
entry: .github/scripts/test-cleanup-runner-resources.sh entry: .github/scripts/test-cleanup-runner-resources.sh
@@ -62,6 +64,30 @@ repos:
language: system language: system
pass_filenames: false pass_filenames: false
files: ^roles/k3s_server/tasks/(main|join_master)\.yml$|^\.github/scripts/test-k3s-server-bootstrap\.sh$ files: ^roles/k3s_server/tasks/(main|join_master)\.yml$|^\.github/scripts/test-k3s-server-bootstrap\.sh$
- id: k3s-init-log-destination-test
name: K3s init log destination test
entry: .github/scripts/test-k3s-init-log-destination.py
language: system
pass_filenames: false
files: ^roles/k3s_server/tasks/main\.yml$|^\.github/scripts/test-k3s-init-log-destination\.py$
- id: unique-hostname-precheck-test
name: Unique hostname precheck test
entry: .github/scripts/test-unique-hostname-precheck.sh
language: system
pass_filenames: false
files: ^site\.yml$|^\.github/scripts/test-unique-hostname-precheck\.sh$
- id: 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: raspberrypi-detection-test
name: Raspberry Pi detection test
entry: .github/scripts/test-raspberrypi-detection.sh
language: system
pass_filenames: false
files: ^roles/raspberrypi/(tasks/main|defaults/main)\.yml$|^\.github/scripts/test-raspberrypi-detection\.sh$
- id: cilium-bgp-manifest-test - id: cilium-bgp-manifest-test
name: Cilium BGP manifest test name: Cilium BGP manifest test
entry: python3 .github/scripts/test-cilium-bgp-manifest.py entry: python3 .github/scripts/test-cilium-bgp-manifest.py
@@ -70,6 +96,15 @@ repos:
- Jinja2>=3.1 - Jinja2>=3.1
pass_filenames: false pass_filenames: false
files: ^roles/k3s_server_post/templates/cilium\.crs\.j2$|^\.github/scripts/test-cilium-bgp-manifest\.py$ files: ^roles/k3s_server_post/templates/cilium\.crs\.j2$|^\.github/scripts/test-cilium-bgp-manifest\.py$
- id: cilium-envoy-toggle-test
name: Cilium Envoy toggle test
entry: python3 .github/scripts/test-cilium-envoy-toggle.py
language: python
additional_dependencies:
- Jinja2>=3.1
- PyYAML
pass_filenames: false
files: ^roles/k3s_server_post/tasks/cilium\.yml$|^\.github/scripts/test-cilium-envoy-toggle\.py$
- id: kube-vip-manifest-test - id: kube-vip-manifest-test
name: kube-vip manifest test name: kube-vip manifest test
entry: python3 .github/scripts/test-kube-vip-manifest.py entry: python3 .github/scripts/test-kube-vip-manifest.py
@@ -84,6 +119,22 @@ repos:
language: system language: system
pass_filenames: false pass_filenames: false
files: ^roles/k3s_server/tasks/metallb\.yml$|^\.github/scripts/test-metallb-remote-read\.sh$ files: ^roles/k3s_server/tasks/metallb\.yml$|^\.github/scripts/test-metallb-remote-read\.sh$
- id: metallb-interfaces-test
name: MetalLB interfaces test
entry: python3 .github/scripts/test-metallb-interfaces.py
language: python
additional_dependencies:
- Jinja2>=3.1
pass_filenames: false
files: ^roles/k3s_server_post/templates/metallb\.crs\.j2$|^\.github/scripts/test-metallb-interfaces\.py$
- id: metallb-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 - id: metallb-deploy-condition-test
name: MetalLB deploy condition test name: MetalLB deploy condition test
entry: python3 .github/scripts/test-metallb-deploy-condition.py entry: python3 .github/scripts/test-metallb-deploy-condition.py
@@ -93,6 +144,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
@@ -101,3 +161,11 @@ repos:
- Jinja2>=3.1 - Jinja2>=3.1
pass_filenames: false pass_filenames: false
files: ^inventory/sample/group_vars/all\.yml$|^\.github/scripts/test-default-interface\.py$ files: ^inventory/sample/group_vars/all\.yml$|^\.github/scripts/test-default-interface\.py$
- id: verify-nginx-assert-test
name: verify nginx assert test
entry: python3 .github/scripts/test-verify-nginx-assert.py
language: python
additional_dependencies:
- PyYAML
pass_filenames: false
files: ^molecule/resources/verify_from_outside/tasks/test/deploy-example\.yml$|^\.github/scripts/test-verify-nginx-assert\.py$ # noqa yaml[line-length]
+9 -1
View File
@@ -50,6 +50,10 @@ Supported processor architectures are:
- Server and agent nodes should support passwordless SSH access. Otherwise, pass `--ask-pass --ask-become-pass` to - Server and agent nodes should support passwordless SSH access. Otherwise, pass `--ask-pass --ask-become-pass` to
each playbook command. each playbook command.
- Every node in the cluster must have a **unique hostname**. k3s registers each node keyed by its hostname, so
two nodes with the same hostname cannot join the cluster. `site.yml` asserts this up front and fails fast if
any duplicate is found.
## 🚀 Getting Started ## 🚀 Getting Started
### 🍴 Preparation ### 🍴 Preparation
@@ -215,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` | 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_bgp_peers_groups` | list | `['k3s_master']` | Not required | Inventory group in which to search for additional `kube_vip_bgp_peers` parameters to merge. |
| `k3s_server` | `kube_vip_iface` | string | `~` | Not required | Explicitly define an interface that ALL control nodes should use to propagate the VIP, define it here. Otherwise, kube-vip will determine the right interface automatically at runtime. | | `k3s_server` | `kube_vip_iface` | string | `~` | Not required | Explicitly define an interface that ALL control nodes should use to propagate the VIP, define it here. Otherwise, kube-vip will determine the right interface automatically at runtime. |
| `k3s_server` | `kube_vip_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_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` |
@@ -257,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) | `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 | | `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` | `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 | | `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`) | | `raspberrypi` | `state` | string | `present` | Not required | Indicates whether the k3s prerequisites for Raspberry Pi should be set up (possible values are `present` and `absent`) |
+29 -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 # this is the user that has ssh access to these machines
ansible_user: ansibleuser ansible_user: ansibleuser
systemd_dir: /etc/systemd/system systemd_dir: /etc/systemd/system
@@ -7,6 +7,10 @@ systemd_dir: /etc/systemd/system
# Set your timezone # Set your timezone
system_timezone: 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 # interface which will be used for flannel
# Defaults to each host's default IPv4 interface (e.g. eth0, enp1s0, ens3) # 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. # so KVM/cloud hosts without eth0 work out of the box. Override per-host if needed.
@@ -24,6 +28,10 @@ cilium_mode: native # native when nodes are on the same subnet or use BGP, other
cilium_tag: v1.20.0 # cilium version tag cilium_tag: v1.20.0 # cilium version tag
cilium_cli_tag: v0.19.7 # cilium cli version tag cilium_cli_tag: v0.19.7 # cilium cli version tag
cilium_hubble: true # enable hubble observability relay and ui cilium_hubble: true # enable hubble observability relay and ui
cilium_envoy: true # enable the Envoy proxy for Cilium L7 policies
# disable cilium_envoy to skip the Envoy proxy entirely (e.g. no L7 policies)
# cilium_envoy: false
# if using calico or cilium, you may specify the cluster pod cidr pool # if using calico or cilium, you may specify the cluster pod cidr pool
cluster_cidr: 10.52.0.0/16 cluster_cidr: 10.52.0.0/16
@@ -40,6 +48,11 @@ cilium_bgp_lb_cidr: 192.168.31.0/24 # cidr for cilium loadbalancer ipam
# enable kube-vip ARP broadcasts # enable kube-vip ARP broadcasts
kube_vip_arp: true 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 # enable kube-vip BGP peering
kube_vip_bgp: false kube_vip_bgp: false
@@ -89,7 +102,11 @@ extra_agent_args: >-
{{ extra_args }} {{ extra_args }}
# image tag for kube-vip # 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 # 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"
@@ -98,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
@@ -116,6 +137,12 @@ metal_lb_controller_tag_version: v0.16.0
# metallb ip range for load balancer # metallb ip range for load balancer
metal_lb_ip_range: 192.168.30.80-192.168.30.90 metal_lb_ip_range: 192.168.30.80-192.168.30.90
# (optional) limit MetalLB layer2 announcements to specific network interfaces.
# Leave empty (default) to announce on all interfaces.
# metal_lb_interfaces:
# - eth1
# - eth2
# Only enable if your nodes are proxmox LXC nodes, make sure to configure your proxmox nodes # Only enable if your nodes are proxmox LXC nodes, make sure to configure your proxmox nodes
# in your hosts.ini file. # in your hosts.ini file.
# Please read https://gist.github.com/triangletodd/02f595cd4c0dc9aac5f7763ca2264185 before using this. # Please read https://gist.github.com/triangletodd/02f595cd4c0dc9aac5f7763ca2264185 before using this.
@@ -69,7 +69,16 @@
url: http://{{ nginx_lb_ip | ansible.utils.ipwrap }}:{{ port_ }}/ url: http://{{ nginx_lb_ip | ansible.utils.ipwrap }}:{{ port_ }}/
return_content: true return_content: true
register: result register: result
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: vars:
port_: >- port_: >-
{{ nginx_services.resources[0].spec.ports[0].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 jmespath>=1.1.0
jsonpatch>=1.33 jsonpatch>=1.35
kubernetes>=29.0.0 kubernetes>=29.0.0
molecule-plugins[vagrant] molecule-plugins[vagrant]
molecule>=6.0.3 molecule>=6.0.3
+3 -3
View File
@@ -6,7 +6,7 @@
# #
ansible-compat==4.1.11 ansible-compat==4.1.11
# via molecule # via molecule
ansible-core==2.19.11 ansible-core==2.19.14
# via # via
# -r requirements.in # -r requirements.in
# ansible-compat # ansible-compat
@@ -55,9 +55,9 @@ jinja2==3.1.3
# molecule # molecule
jmespath==1.1.0 jmespath==1.1.0
# via -r requirements.in # via -r requirements.in
jsonpatch==1.33 jsonpatch==1.35
# via -r requirements.in # via -r requirements.in
jsonpointer==2.4 jsonpointer==3.2.1
# via jsonpatch # via jsonpatch
jsonschema==4.21.1 jsonschema==4.21.1
# via # via
+6 -2
View File
@@ -7,8 +7,9 @@ group_name_master: master
kube_vip_arp: true kube_vip_arp: true
kube_vip_iface: kube_vip_iface:
kube_vip_endpoint:
kube_vip_cloud_provider_tag_version: v0.0.12 kube_vip_cloud_provider_tag_version: v0.0.12
kube_vip_tag_version: v1.2.2 kube_vip_tag_version: v1.2.3
kube_vip_bgp: false kube_vip_bgp: false
kube_vip_bgp_routerid: 127.0.0.1 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: []
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
@@ -34,7 +38,7 @@ retry_count: 20
# yamllint disable rule:line-length # yamllint disable rule:line-length
server_init_args: >- server_init_args: >-
{% if groups[group_name_master | default('master')] | length > 1 %} {% if groups[group_name_master | default('master')] | length > 1 %}
{% if ansible_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 --cluster-init
{% else %} {% else %}
--server https://{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(",") | first | ansible.utils.ipwrap }}:6443 --server https://{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(",") | first | ansible.utils.ipwrap }}:6443
+25 -1
View File
@@ -78,18 +78,42 @@ argument_specs:
- automatically at runtime. - automatically at runtime.
default: ~ 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: kube_vip_tag_version:
description: Image tag for kube-vip description: Image tag for kube-vip
default: v1.2.2 default: v1.2.3
kube_vip_cloud_provider_tag_version: kube_vip_cloud_provider_tag_version:
description: Tag for kube-vip-cloud-provider manifest when enabled description: Tag for kube-vip-cloud-provider manifest when enabled
default: v0.0.12 default: v0.0.12
kube_vip_enabled:
description:
- Enable installing kube-vip.
- Covers both the control-plane VIP and the service load balancer.
- Set false to skip kube-vip entirely, for example on a single node or when an external load balancer is used.
default: true
type: bool
kube_vip_lb_ip_range: kube_vip_lb_ip_range:
description: IP range for kube-vip load balancer description: IP range for kube-vip load balancer
default: ~ default: ~
metal_lb_enabled:
description:
- Enable installing MetalLB for service load balancing.
- Set false to skip MetalLB, for example when an external load balancer is used.
default: true
type: bool
metal_lb_controller_tag_version: metal_lb_controller_tag_version:
description: Image tag for MetalLB description: Image tag for MetalLB
default: v0.16.0 default: v0.16.0
@@ -83,7 +83,7 @@
no_log: true no_log: true
register: k3s_input_firewall_rules register: k3s_input_firewall_rules
- name: Ping the primary Kubernetes API address from {{ ansible_hostname }} - name: Ping the primary Kubernetes API address from {{ ansible_facts.hostname }}
ansible.builtin.command: ansible.builtin.command:
argv: argv:
- ping - ping
@@ -97,7 +97,7 @@
no_log: true no_log: true
register: k3s_primary_api_ping register: k3s_primary_api_ping
- name: Probe primary Kubernetes API from {{ ansible_hostname }} - name: Probe primary Kubernetes API from {{ ansible_facts.hostname }}
ansible.builtin.wait_for: ansible.builtin.wait_for:
host: "{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(',') | first }}" host: "{{ hostvars[groups[group_name_master | default('master')][0]].k3s_node_ip | split(',') | first }}"
port: 6443 port: 6443
@@ -122,7 +122,7 @@
become: false become: false
ansible.builtin.template: ansible.builtin.template:
src: content.j2 src: content.j2
dest: "{{ log_destination }}/k3s-init@{{ ansible_hostname }}.log" dest: "{{ log_destination }}/k3s-init@{{ ansible_facts['hostname'] }}.log"
mode: "0644" mode: "0644"
vars: vars:
content: | content: |
@@ -137,7 +137,7 @@
become: false become: false
ansible.builtin.template: ansible.builtin.template:
src: content.j2 src: content.j2
dest: "{{ log_destination }}/network@{{ ansible_hostname }}.log" dest: "{{ log_destination }}/network@{{ ansible_facts['hostname'] }}.log"
mode: "0644" mode: "0644"
vars: vars:
content: | content: |
+1 -1
View File
@@ -25,7 +25,7 @@
ansible.builtin.command: ansible.builtin.command:
cmd: >- cmd: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} get node {{ k3s_kubectl_binary | default('k3s kubectl') }} get node
{{ hostvars[joining_master].ansible_hostname }} {{ hostvars[joining_master].ansible_facts['hostname'] }}
delegate_to: "{{ groups[group_name_master | default('master')][0] }}" delegate_to: "{{ groups[group_name_master | default('master')][0] }}"
register: joined_master register: joined_master
until: joined_master.rc == 0 until: joined_master.rc == 0
+6 -3
View File
@@ -6,7 +6,8 @@
owner: root owner: root
group: root group: root
mode: "0644" 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 - name: Download vip cloud provider manifest to first master
ansible.builtin.get_url: ansible.builtin.get_url:
@@ -19,7 +20,8 @@
retries: "{{ download_retries }}" retries: "{{ download_retries }}"
delay: "{{ download_delay }}" delay: "{{ download_delay }}"
until: kube_vip_manifest_download is succeeded until: kube_vip_manifest_download is succeeded
when: 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 - name: Copy kubevip configMap manifest to first master
ansible.builtin.template: ansible.builtin.template:
@@ -28,4 +30,5 @@
owner: root owner: root
group: root group: root
mode: "0644" 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 - 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
@@ -102,7 +104,7 @@
expected_control_plane_names: >- expected_control_plane_names: >-
{{ {{
groups[group_name_master | default('master')] groups[group_name_master | default('master')]
| map('extract', hostvars, 'ansible_hostname') | map('extract', hostvars, ['ansible_facts', 'hostname'])
| list | list
}} }}
@@ -122,13 +124,18 @@
always: always:
- name: Save logs of k3s-init.service - name: Save logs of k3s-init.service
ansible.builtin.include_tasks: fetch_k3s_init_logs.yml ansible.builtin.include_tasks: fetch_k3s_init_logs.yml
# ANSIBLE_K3S_LOG_DIR is a path string when set; evaluate it as a boolean # The env lookup default must be an empty string, not the boolean
# so the conditional is a real boolean (ansible-core 2.19 rejects string # False. The `!= ''` comparison below always evaluates to a bool, so
# conditionals derived from env vars). # 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('') != '' when: log_destination | default('') != ''
vars: vars:
log_destination: >- 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 - name: Kill the temporary service used for initialization
ansible.builtin.systemd: ansible.builtin.systemd:
name: k3s-init name: k3s-init
@@ -181,24 +188,24 @@
- name: Create directory .kube - name: Create directory .kube
ansible.builtin.file: ansible.builtin.file:
path: "{{ ansible_user_dir }}/.kube" path: "{{ ansible_facts['user_dir'] }}/.kube"
state: directory state: directory
owner: "{{ ansible_user_id }}" owner: "{{ ansible_facts['user_id'] }}"
mode: u=rwx,g=rx,o= mode: u=rwx,g=rx,o=
- name: Copy config file to user home directory - name: Copy config file to user home directory
ansible.builtin.copy: ansible.builtin.copy:
src: /etc/rancher/k3s/k3s.yaml src: /etc/rancher/k3s/k3s.yaml
dest: "{{ ansible_user_dir }}/.kube/config" dest: "{{ ansible_facts['user_dir'] }}/.kube/config"
remote_src: true remote_src: true
owner: "{{ ansible_user_id }}" owner: "{{ ansible_facts['user_id'] }}"
mode: u=rw,g=,o= mode: u=rw,g=,o=
- name: Configure kubectl cluster to {{ endpoint_url }} - name: Configure kubectl cluster to {{ endpoint_url }}
ansible.builtin.command: >- ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} config set-cluster default {{ k3s_kubectl_binary | default('k3s kubectl') }} config set-cluster default
--server={{ endpoint_url }} --server={{ endpoint_url }}
--kubeconfig {{ ansible_user_dir }}/.kube/config --kubeconfig {{ ansible_facts['user_dir'] }}/.kube/config
changed_when: true changed_when: true
vars: vars:
endpoint_url: >- endpoint_url: >-
+12 -6
View File
@@ -6,7 +6,8 @@
owner: root owner: root
group: root group: root
mode: "0644" 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 }}" - name: "Download to first master: manifest for metallb-{{ metal_lb_type }}"
ansible.builtin.get_url: ansible.builtin.get_url:
@@ -19,7 +20,8 @@
retries: "{{ download_retries }}" retries: "{{ download_retries }}"
delay: "{{ download_delay }}" delay: "{{ download_delay }}"
until: metallb_manifest_download is succeeded until: metallb_manifest_download is succeeded
when: 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 }} - name: Set image versions in manifest for metallb-{{ metal_lb_type }}
ansible.builtin.replace: ansible.builtin.replace:
@@ -31,13 +33,15 @@
to: metallb/speaker:{{ metal_lb_speaker_tag_version }} to: metallb/speaker:{{ metal_lb_speaker_tag_version }}
loop_control: loop_control:
label: "{{ item.change }} => {{ item.to }}" label: "{{ item.change }} => {{ item.to }}"
when: 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 - name: Read back MetalLB manifest from first master
ansible.builtin.slurp: ansible.builtin.slurp:
src: /var/lib/rancher/k3s/server/manifests/metallb-crds.yaml src: /var/lib/rancher/k3s/server/manifests/metallb-crds.yaml
register: metallb_manifest register: metallb_manifest
when: 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 - name: Check the MetalLB speaker image reference is present
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -46,7 +50,8 @@
('quay.io/metallb/speaker:' ~ metal_lb_speaker_tag_version) in ('quay.io/metallb/speaker:' ~ metal_lb_speaker_tag_version) in
(metallb_manifest.content | default('') | b64decode) (metallb_manifest.content | default('') | b64decode)
}} }}
when: 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 - name: Fail if MetalLB speaker tag was not applied to the manifest
ansible.builtin.fail: ansible.builtin.fail:
@@ -56,5 +61,6 @@
was not found in the downloaded manifest. The upstream image was not found in the downloaded manifest. The upstream image
reference may have changed. reference may have changed.
when: when:
- ansible_hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_hostname'] - >-
ansible_facts.hostname == hostvars[groups[group_name_master | default('master')][0]]['ansible_facts']['hostname']
- not metallb_manifest_has_speaker_tag - not metallb_manifest_has_speaker_tag
+6 -3
View File
@@ -10,7 +10,8 @@
owner: root owner: root
group: root group: root
mode: "0644" 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 - name: Copy kube-vip RBAC manifest to first master
ansible.builtin.template: ansible.builtin.template:
@@ -19,7 +20,8 @@
owner: root owner: root
group: root group: root
mode: "0644" 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 - name: Copy vip manifest to first master
ansible.builtin.template: ansible.builtin.template:
@@ -28,4 +30,5 @@
owner: root owner: root
group: root group: root
mode: "0644" 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']
+2 -2
View File
@@ -37,7 +37,7 @@ spec:
value: {{ kube_vip_iface }} value: {{ kube_vip_iface }}
{% endif %} {% endif %}
- name: vip_subnet - 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 - name: cp_enable
value: "true" value: "true"
- name: cp_namespace - name: cp_namespace
@@ -55,7 +55,7 @@ spec:
- name: vip_retryperiod - name: vip_retryperiod
value: "2" value: "2"
- name: address - name: address
value: {{ apiserver_endpoint }} value: {{ kube_vip_endpoint | default(apiserver_endpoint, true) }}
{% if kube_vip_bgp | default(false) | bool %} {% if kube_vip_bgp | default(false) | bool %}
{% if kube_vip_bgp_routerid is defined %} {% if kube_vip_bgp_routerid is defined %}
- name: bgp_routerid - name: bgp_routerid
+3
View File
@@ -18,6 +18,7 @@ cilium_bgp_peer_asn: 64512
cilium_bgp_neighbors: [] cilium_bgp_neighbors: []
cilium_bgp_neighbors_groups: ['k3s_all'] cilium_bgp_neighbors_groups: ['k3s_all']
cilium_bgp_lb_cidr: 192.168.31.0/24 cilium_bgp_lb_cidr: 192.168.31.0/24
cilium_envoy: true
cilium_hubble: true cilium_hubble: true
cilium_mode: native cilium_mode: native
cilium_tag: v1.20.0 cilium_tag: v1.20.0
@@ -38,4 +39,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_ip_range: 192.168.30.80-192.168.30.90 metal_lb_ip_range: 192.168.30.80-192.168.30.90
+15
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
@@ -141,6 +148,14 @@ argument_specs:
description: MetalLB ip range for load balancer description: MetalLB ip range for load balancer
default: 192.168.30.80-192.168.30.90 default: 192.168.30.80-192.168.30.90
metal_lb_interfaces:
description: >-
List of network interfaces on which MetalLB should announce the
load balancer IPs in layer2 mode. When empty (default), MetalLB
announces on all interfaces.
type: list
default: []
metal_lb_controller_tag_version: metal_lb_controller_tag_version:
description: Image tag for MetalLB description: Image tag for MetalLB
default: v0.16.0 default: v0.16.0
+2 -1
View File
@@ -1,6 +1,7 @@
--- ---
- name: Deploy Calico to cluster - 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 run_once: true
block: block:
- name: Create manifests directory on first master - name: Create manifests directory on first master
+3 -2
View File
@@ -178,12 +178,13 @@
--helm-set hubble.enabled={{ "true" if cilium_hubble else "false" }} --helm-set hubble.enabled={{ "true" if cilium_hubble else "false" }}
--helm-set hubble.relay.enabled={{ "true" if cilium_hubble else "false" }} --helm-set hubble.relay.enabled={{ "true" if cilium_hubble else "false" }}
--helm-set hubble.ui.enabled={{ "true" if cilium_hubble else "false" }} --helm-set hubble.ui.enabled={{ "true" if cilium_hubble else "false" }}
--helm-set envoy.enabled={{ "true" if cilium_envoy else "false" }}
{% if kube_proxy_replacement is not false %} {% if kube_proxy_replacement is not false %}
--helm-set loadBalancer.algorithm={{ bpf_lb_algorithm }} --helm-set loadBalancer.algorithm={{ bpf_lb_algorithm }}
--helm-set loadBalancer.mode={{ bpf_lb_mode }} --helm-set loadBalancer.mode={{ bpf_lb_mode }}
{% endif %} {% endif %}
environment: environment:
KUBECONFIG: "{{ ansible_user_dir }}/.kube/config" KUBECONFIG: "{{ ansible_facts['user_dir'] }}/.kube/config"
register: cilium_install_result register: cilium_install_result
changed_when: cilium_install_result.rc == 0 changed_when: cilium_install_result.rc == 0
# cilium install/upgrade fetches the Helm chart from helm.cilium.io, which is # cilium install/upgrade fetches the Helm chart from helm.cilium.io, which is
@@ -226,7 +227,7 @@
- name: Wait for Cilium status to be healthy - name: Wait for Cilium status to be healthy
ansible.builtin.command: cilium status --wait ansible.builtin.command: cilium status --wait
environment: environment:
KUBECONFIG: "{{ ansible_user_dir }}/.kube/config" KUBECONFIG: "{{ ansible_facts['user_dir'] }}/.kube/config"
register: cilium_status register: cilium_status
changed_when: false changed_when: false
until: cilium_status.rc == 0 until: cilium_status.rc == 0
+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:
+15 -3
View File
@@ -3,7 +3,7 @@
ansible.builtin.file: ansible.builtin.file:
path: /tmp/k3s path: /tmp/k3s
state: directory state: directory
owner: "{{ ansible_user_id }}" owner: "{{ ansible_facts['user_id'] }}"
mode: "0755" mode: "0755"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
@@ -33,15 +33,21 @@
ansible.builtin.template: ansible.builtin.template:
src: metallb.crs.j2 src: metallb.crs.j2
dest: /tmp/k3s/metallb-crs.yaml dest: /tmp/k3s/metallb-crs.yaml
owner: "{{ ansible_user_id }}" owner: "{{ ansible_facts['user_id'] }}"
mode: "0755" mode: "0755"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
- name: Test metallb-system namespace - name: Test metallb-system namespace
ansible.builtin.command: >- 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 changed_when: false
# The kube API can briefly return ServiceUnavailable while MetalLB converges,
# which would otherwise abort the whole converge play on a transient error.
register: metallb_namespace_result
until: metallb_namespace_result.rc == 0
retries: "{{ download_retries }}"
delay: "{{ download_delay }}"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
@@ -99,6 +105,12 @@
ansible.builtin.command: >- ansible.builtin.command: >-
{{ k3s_kubectl_binary | default('k3s kubectl') }} -n metallb-system get endpoints {{ metallb_webhook_service_name }} {{ k3s_kubectl_binary | default('k3s kubectl') }} -n metallb-system get endpoints {{ metallb_webhook_service_name }}
changed_when: false changed_when: false
# The kube API can briefly return ServiceUnavailable while MetalLB converges,
# which would otherwise abort the whole converge play on a transient error.
register: metallb_webhook_result
until: metallb_webhook_result.rc == 0
retries: "{{ download_retries }}"
delay: "{{ download_delay }}"
with_items: "{{ groups[group_name_master | default('master')] }}" with_items: "{{ groups[group_name_master | default('master')] }}"
run_once: true run_once: true
@@ -21,6 +21,11 @@ kind: L2Advertisement
metadata: metadata:
name: default name: default
namespace: metallb-system namespace: metallb-system
{% if metal_lb_interfaces | default([]) | length > 0 %}
spec:
interfaces:{% for iface in metal_lb_interfaces %}
- {{ iface }}{% endfor %}
{% endif %}
{% endif %} {% endif %}
{% if metal_lb_mode == "bgp" %} {% if metal_lb_mode == "bgp" %}
--- ---
+2
View File
@@ -1,4 +1,6 @@
--- ---
disable_swap: true
secure_path: secure_path:
RedHat: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin RedHat: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
Suse: /usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin Suse: /usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin
+39 -9
View File
@@ -4,10 +4,32 @@
name: "{{ system_timezone }}" name: "{{ system_timezone }}"
when: (system_timezone is defined) and (system_timezone != "Your/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 - name: Set SELinux to disabled state
ansible.posix.selinux: ansible.posix.selinux:
state: disabled state: disabled
when: ansible_os_family == "RedHat" when: ansible_facts.os_family == "RedHat"
- name: Enable IPv4 forwarding - name: Enable IPv4 forwarding
ansible.posix.sysctl: ansible.posix.sysctl:
@@ -41,31 +63,39 @@
executable: /bin/bash executable: /bin/bash
register: br_netfilter_exists register: br_netfilter_exists
changed_when: false changed_when: false
when: ansible_os_family == "RedHat" when: ansible_facts.os_family == "RedHat"
tags: sysctl tags: sysctl
- name: Install kernel-modules-extra if br_netfilter missing - name: Install kernel-modules-extra if br_netfilter missing
ansible.builtin.yum: ansible.builtin.yum:
name: kernel-modules-extra name: "kernel-modules-extra-{{ ansible_kernel }}"
state: present state: present
update_cache: true update_cache: true
register: kernel_modules_installed register: kernel_modules_installed
when: when:
- ansible_os_family == "RedHat" - ansible_facts.os_family == "RedHat"
- br_netfilter_exists.stdout | int == 0 - br_netfilter_exists.stdout | int == 0
- name: Reboot node if kernel modules package was installed
ansible.builtin.reboot:
msg: "Rebooting to apply new kernel modules package"
reboot_timeout: 300
when:
- ansible_facts.os_family == "RedHat"
- kernel_modules_installed is changed
- name: Add br_netfilter to /etc/modules-load.d/ - name: Add br_netfilter to /etc/modules-load.d/
ansible.builtin.copy: ansible.builtin.copy:
content: br_netfilter content: br_netfilter
dest: /etc/modules-load.d/br_netfilter.conf dest: /etc/modules-load.d/br_netfilter.conf
mode: u=rw,g=,o= mode: u=rw,g=,o=
when: ansible_os_family == "RedHat" when: ansible_facts.os_family == "RedHat"
- name: Load br_netfilter - name: Load br_netfilter
community.general.modprobe: community.general.modprobe:
name: br_netfilter name: br_netfilter
state: present state: present
when: ansible_os_family == "RedHat" when: ansible_facts.os_family == "RedHat"
- name: Set bridge-nf-call-iptables (just to be sure) - name: Set bridge-nf-call-iptables (just to be sure)
ansible.posix.sysctl: ansible.posix.sysctl:
@@ -73,7 +103,7 @@
value: "1" value: "1"
state: present state: present
reload: true reload: true
when: ansible_os_family == "RedHat" when: ansible_facts.os_family == "RedHat"
loop: loop:
- net.bridge.bridge-nf-call-iptables - net.bridge.bridge-nf-call-iptables
- net.bridge.bridge-nf-call-ip6tables - net.bridge.bridge-nf-call-ip6tables
@@ -81,10 +111,10 @@
- name: Add /usr/local/bin to sudo secure_path - name: Add /usr/local/bin to sudo secure_path
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
line: Defaults secure_path = {{ secure_path[ansible_os_family] }} line: Defaults secure_path = {{ secure_path[ansible_facts.os_family] }}
regexp: Defaults(\s)*secure_path(\s)*= regexp: Defaults(\s)*secure_path(\s)*=
state: present state: present
insertafter: EOF insertafter: EOF
path: /etc/sudoers path: /etc/sudoers
validate: visudo -cf %s validate: visudo -cf %s
when: ansible_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 - name: Test for raspberry pi /proc/cpuinfo
ansible.builtin.command: grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo ansible.builtin.command: grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo
register: grep_cpuinfo_raspberrypi register: grep_cpuinfo_raspberrypi
failed_when: false failed_when: false
changed_when: false changed_when: false
check_mode: false
- name: Test for raspberry pi /proc/device-tree/model - name: Test for raspberry pi /proc/device-tree/model
ansible.builtin.command: grep -E "Raspberry Pi" /proc/device-tree/model ansible.builtin.command: grep -E "Raspberry Pi" /proc/device-tree/model
register: grep_device_tree_model_raspberrypi register: grep_device_tree_model_raspberrypi
failed_when: false failed_when: false
changed_when: false changed_when: false
check_mode: false
- name: Set raspberry_pi fact to true - name: Set raspberry_pi fact to true
ansible.builtin.set_fact: ansible.builtin.set_fact:
raspberry_pi: true raspberry_pi: true
when: grep_cpuinfo_raspberrypi.rc == 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) - name: Set detected_distribution to Raspbian (ARM64 on Raspbian, Debian Buster/Bullseye/Bookworm)
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -49,8 +56,8 @@
with_first_found: with_first_found:
- "{{ action_ }}/{{ detected_distribution }}-{{ detected_distribution_major_version }}.yml" - "{{ action_ }}/{{ detected_distribution }}-{{ detected_distribution_major_version }}.yml"
- "{{ action_ }}/{{ detected_distribution }}.yml" - "{{ action_ }}/{{ detected_distribution }}.yml"
- "{{ action_ }}/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml" - "{{ action_ }}/{{ ansible_facts['distribution'] }}-{{ ansible_facts['distribution_major_version'] }}.yml"
- "{{ action_ }}/{{ ansible_distribution }}.yml" - "{{ action_ }}/{{ ansible_facts['distribution'] }}.yml"
- "{{ action_ }}/default.yml" - "{{ action_ }}/default.yml"
vars: vars:
action_: >- action_: >-
+24 -3
View File
@@ -8,6 +8,23 @@
msg: > msg: >
"Ansible is out of date. See here for more info: https://docs.technotim.com/posts/ansible-automation/" "Ansible is out of date. See here for more info: https://docs.technotim.com/posts/ansible-automation/"
- name: Verify all cluster nodes have unique hostnames
ansible.builtin.assert:
that: (cluster_hostnames | unique | length) == (cluster_hostnames | length)
msg: >-
k3s nodes must have unique hostnames. Found a duplicate in:
{{ cluster_hostnames | unique }}. Each node registers in the cluster
keyed by its hostname, so matching hostnames prevent nodes from joining.
vars:
cluster_hostnames: >-
{{
groups['k3s_cluster']
| map('extract', hostvars, ['ansible_facts', 'hostname'])
| list
}}
run_once: true
when: "'k3s_cluster' in groups"
- name: Prepare Proxmox cluster - name: Prepare Proxmox cluster
hosts: proxmox hosts: proxmox
gather_facts: true gather_facts: true
@@ -60,9 +77,13 @@
hosts: master hosts: master
environment: "{{ proxy_env | default({}) }}" environment: "{{ proxy_env | default({}) }}"
tasks: 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: ansible.builtin.fetch:
src: "{{ ansible_user_dir }}/.kube/config" src: "{{ ansible_facts['user_dir'] }}/.kube/config"
dest: ./kubeconfig dest: ./kubeconfig
flat: true 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']