feat: switch to binary kubernetes, fluxcd and helm install (#351)

* feat: more binary installs

* feat: install k8s from binaries

* fix: sync with the main branch

* fix(containerd): go back to using ansible_system

* fix(containerd): containerd+crictl cleanups

* chore: refactor k8s role

* ci: fix job name

* ci: do not fail-fast

* ci: disable swap

* ci: disable sudo

* ci: add kubelet logs

* ci: install udev

* ci: fix package names

* ci: fix idempotence

* ci: install deps earlier

* ci: added k8s tests

* ci: fix vars for fedora

* chore: drop unused submodule

* ci: fix typo in kubelet

* ci: start dbus.socket

* ci: fix fedora

* ci: fix paths

* fix: add maxconn to avoid killing system

* ci: print container logs

* ci: fix role test

* ci: move to stdout logs

* ci: fix idempotence

* ci: capture both stdout+stderr

* ci: drop extra default-server

* ci: fix haproxy

* ci: install apparmor-utils

* ci: update apt cache

* ci: remove pyyaml from rocky linux

* ci: add ha tests

* ci: fix flipped scenarios

* ci: use default keepalived iface

* chore: add debug

* chore: start tmate on failure

* chore: use newer containerd

* chore: fix shas

* ci: fix debian

* ci: back to debug

* ci: add containerd test suite

* ci: fix idempotence

* ci: force containerd restart

* ci: drop handler

* ci: load ip_tables module

* ci: add modprobe

* ci: add missing pkgs

* ci: load ip6_tables

* ci: add /lib/modules

* ci: add missing udev

* ci: run unconfined apparmor

* ci: drop debian + fedora support

* ci: fix paths

* chore: refactor to use vexxhost.kubernetes

* chore: refactor to using helm role

* wip

* ci: remove un-needed tests

* chore: refactor to k8s_node_label

* chore: fix k8s deploy

---------

Co-authored-by: Mohammed Naser <mnaser@vexxhost.com>
diff --git a/galaxy.yml b/galaxy.yml
index 9c587ce..87c5c5e 100644
--- a/galaxy.yml
+++ b/galaxy.yml
@@ -16,7 +16,7 @@
   kubernetes.core: 2.3.2
   openstack.cloud: 1.7.0
   community.mysql: 3.6.0
-  vexxhost.kubernetes: 1.3.0
+  vexxhost.kubernetes: 1.3.3
 tags:
   - application
   - cloud
diff --git a/playbooks/cni.yml b/playbooks/cni.yml
deleted file mode 100644
index 9f694d1..0000000
--- a/playbooks/cni.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright (c) 2023 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- hosts: controllers[0]
-  gather_facts: false
-  become: true
-  roles:
-    - role: defaults
-    - role: vexxhost.kubernetes.cilium
-      vars:
-        cilium_node_image: "{{ atmosphere_images['cilium_node'] }}"
-        cilium_operator_image: "{{ atmosphere_images['cilium_operator'] }}"
-      tags:
-        - cilium
diff --git a/playbooks/flux.yml b/playbooks/flux.yml
deleted file mode 100644
index 37ce499..0000000
--- a/playbooks/flux.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) 2023 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- hosts: controllers
-  gather_facts: false
-  become: true
-  roles:
-    - role: defaults
-    - role: vexxhost.kubernetes.flux
-      vars:
-        flux_image_registry: "{{ atmosphere_images['flux_helm_controller'] | vexxhost.kubernetes.docker_image('prefix') }}"
-      tags:
-        - flux
diff --git a/playbooks/helm.yml b/playbooks/helm.yml
deleted file mode 100644
index 35a38c0..0000000
--- a/playbooks/helm.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright (c) 2023 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- hosts: controllers
-  become: true
-  roles:
-    - vexxhost.kubernetes.helm
diff --git a/playbooks/kubernetes.yml b/playbooks/kubernetes.yml
index 14806d9..502f91e 100644
--- a/playbooks/kubernetes.yml
+++ b/playbooks/kubernetes.yml
@@ -15,14 +15,35 @@
 - hosts: "{{ target | default('all') }}"
   become: true
   roles:
+    - role: defaults
     - role: sysctl
     - role: ethtool
       tags:
         - ethtool
-    - role: vexxhost.kubernetes.containerd
-      vars:
-        containerd_pause_image: "{{ atmosphere_images['pause'] }}"
-    - role: kubernetes
+  post_tasks:
+    - name: Set a fact with the "atmosphere_images" for other plays
+      ansible.builtin.set_fact:
+        atmosphere_images: "{{ atmosphere_images }}"
+
+- import_playbook: vexxhost.kubernetes.site
+  vars:
+    keepalived_image: "{{ atmosphere_images['keepalived'] }}"
+    keepalived_vrid: "{{ kubernetes_keepalived_vrid }}"
+    keepalived_interface: "{{ kubernetes_keepalived_interface }}"
+    keepalived_vip: "{{ kubernetes_keepalived_vip }}"
+    haproxy_image: "{{ atmosphere_images['haproxy'] }}"
+    containerd_pause_image: "{{ atmosphere_images['pause'] }}"
+    kubernetes_image_repository: "{{ atmosphere_images['kube_apiserver'] | vexxhost.kubernetes.docker_image('prefix') }}"
+    cilium_node_image: "{{ atmosphere_images['cilium_node'] }}"
+    cilium_operator_image: "{{ atmosphere_images['cilium_operator'] }}"
+    flux_image_registry: "{{ atmosphere_images['flux_helm_controller'] | vexxhost.kubernetes.docker_image('prefix') }}"
+
+- hosts: "{{ target | default('all') }}"
+  become: true
+  roles:
+    - role: vexxhost.atmosphere.kubernetes_node_labels
+      tags:
+        - kubernetes-node-labels
 
 # NOTE(mnaser): Add task to uninstall "unattended-upgrades" to avoid system
 #               upgrades during the deployment
diff --git a/playbooks/site.yml b/playbooks/site.yml
index 545227f..c8fca05 100644
--- a/playbooks/site.yml
+++ b/playbooks/site.yml
@@ -14,8 +14,5 @@
 
 - import_playbook: vexxhost.atmosphere.ceph
 - import_playbook: vexxhost.atmosphere.kubernetes
-- import_playbook: vexxhost.atmosphere.helm
-- import_playbook: vexxhost.atmosphere.cni
-- import_playbook: vexxhost.atmosphere.flux
 - import_playbook: vexxhost.atmosphere.csi
 - import_playbook: vexxhost.atmosphere.openstack
diff --git a/plugins/filter/from_ini.py b/plugins/filter/from_ini.py
index d77e3ea..23fd829 100644
--- a/plugins/filter/from_ini.py
+++ b/plugins/filter/from_ini.py
@@ -66,7 +66,7 @@
 
     def _parse_section(section):
         data = dict(section)
-        data.pop('__name__', None)
+        data.pop("__name__", None)
         for opt, val in data.items():
             if val.isdigit():
                 val = int(val)
diff --git a/roles/defaults/defaults/main.yml b/roles/defaults/defaults/main.yml
index 0608333..35fac6b 100644
--- a/roles/defaults/defaults/main.yml
+++ b/roles/defaults/defaults/main.yml
@@ -30,8 +30,8 @@
   cert_manager_controller: quay.io/jetstack/cert-manager-controller:v1.7.1
   cert_manager_webhook: quay.io/jetstack/cert-manager-webhook:v1.7.1
   ceph: quay.io/ceph/ceph:v16.2.11
-  cilium_node: quay.io/cilium/cilium:v1.10.7@sha256:e23f55e80e1988db083397987a89967aa204ad6fc32da243b9160fbcea29b0ca
-  cilium_operator: quay.io/cilium/operator-generic:v1.10.7@sha256:d0b491d8d8cb45862ed7f0410f65e7c141832f0f95262643fa5ff1edfcddcafe
+  cilium_node: quay.io/cilium/cilium:v1.10.20@sha256:c9b3af1f9c405cc8dcb163af0e0ea0a376a9f62304501c9392d26e91178d7869
+  cilium_operator: quay.io/cilium/operator-generic:v1.10.20@sha256:f7a07e674687fc4be01168409eeed0986ed7cb19c8d966501507fd24b9f6bd98
   cinder_api: quay.io/vexxhost/cinder:zed
   cinder_backup_storage_init: quay.io/vexxhost/cinder:zed
   cinder_backup: quay.io/vexxhost/cinder:zed
@@ -44,12 +44,12 @@
   cluster_api_kubeadm_bootstrap_controller: registry.k8s.io/cluster-api/kubeadm-bootstrap-controller:v1.3.3
   cluster_api_kubeadm_control_plane_controller: registry.k8s.io/cluster-api/kubeadm-control-plane-controller:v1.3.3
   cluster_api_openstack_controller: quay.io/vexxhost/capi-openstack-controller:v0.7.1-1
-  csi_node_driver_registrar: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v2.4.0
-  csi_rbd_attacher: k8s.gcr.io/sig-storage/csi-attacher:v3.4.0
+  csi_node_driver_registrar: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.4.0
+  csi_rbd_attacher: registry.k8s.io/sig-storage/csi-attacher:v3.4.0
   csi_rbd_plugin: quay.io/cephcsi/cephcsi:v3.5.1
-  csi_rbd_provisioner: k8s.gcr.io/sig-storage/csi-provisioner:v3.1.0
-  csi_rbd_resizer: k8s.gcr.io/sig-storage/csi-resizer:v1.3.0
-  csi_rbd_snapshotter: k8s.gcr.io/sig-storage/csi-snapshotter:v4.2.0
+  csi_rbd_provisioner: registry.k8s.io/sig-storage/csi-provisioner:v3.1.0
+  csi_rbd_resizer: registry.k8s.io/sig-storage/csi-resizer:v1.3.0
+  csi_rbd_snapshotter: registry.k8s.io/sig-storage/csi-snapshotter:v4.2.0
   db_drop: quay.io/vexxhost/heat:zed
   db_init: quay.io/vexxhost/heat:zed
   dep_check: quay.io/vexxhost/kubernetes-entrypoint:latest
@@ -81,9 +81,9 @@
   heat_purge_deleted: quay.io/vexxhost/heat:zed
   horizon_db_sync: quay.io/vexxhost/horizon:zed
   horizon: quay.io/vexxhost/horizon:zed
-  ingress_nginx_controller: k8s.gcr.io/ingress-nginx/controller:v1.1.1@sha256:0bc88eb15f9e7f84e8e56c14fa5735aaa488b840983f87bd79b1054190e660de
-  ingress_nginx_default_backend: k8s.gcr.io/defaultbackend-amd64:1.5
-  ingress_nginx_kube_webhook_certgen: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660 # noqa: yaml[line-length]
+  ingress_nginx_controller: registry.k8s.io/ingress-nginx/controller:v1.1.1@sha256:0bc88eb15f9e7f84e8e56c14fa5735aaa488b840983f87bd79b1054190e660de
+  ingress_nginx_default_backend: registry.k8s.io/defaultbackend-amd64:1.5
+  ingress_nginx_kube_webhook_certgen: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660 # noqa: yaml[line-length]
   keepalived: us-docker.pkg.dev/vexxhost-infra/openstack/keepalived:2.0.19
   keystone_api: quay.io/vexxhost/keystone:zed
   keystone_credential_cleanup: quay.io/vexxhost/heat:zed
@@ -96,12 +96,12 @@
   ks_endpoints: quay.io/vexxhost/heat:zed
   ks_service: quay.io/vexxhost/heat:zed
   ks_user: quay.io/vexxhost/heat:zed
-  kube_apiserver: k8s.gcr.io/kube-apiserver:v1.22.17
-  kube_controller_manager: k8s.gcr.io/kube-controller-manager:v1.22.17
-  kube_coredns: k8s.gcr.io/coredns/coredns:v1.8.4
-  kube_etcd: k8s.gcr.io/etcd:3.5.6-0
-  kube_proxy: k8s.gcr.io/kube-proxy:v1.22.17
-  kube_scheduler: k8s.gcr.io/kube-scheduler:v1.22.17
+  kube_apiserver: registry.k8s.io/kube-apiserver:v1.22.17
+  kube_controller_manager: registry.k8s.io/kube-controller-manager:v1.22.17
+  kube_coredns: registry.k8s.io/coredns/coredns:v1.8.4
+  kube_etcd: registry.k8s.io/etcd:3.5.6-0
+  kube_proxy: registry.k8s.io/kube-proxy:v1.22.17
+  kube_scheduler: registry.k8s.io/kube-scheduler:v1.22.17
   kube_state_metrics: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.6.0
   libvirt: quay.io/vexxhost/libvirtd:yoga-focal
   loki: docker.io/grafana/loki:2.7.3
@@ -130,7 +130,7 @@
   neutron_server: quay.io/vexxhost/neutron:zed
   neutron_sriov_agent_init: quay.io/vexxhost/neutron:zed
   neutron_sriov_agent: quay.io/vexxhost/neutron:zed
-  node_feature_discovery: k8s.gcr.io/nfd/node-feature-discovery:v0.11.2
+  node_feature_discovery: registry.k8s.io/nfd/node-feature-discovery:v0.11.2
   nova_api: quay.io/vexxhost/nova:zed
   nova_archive_deleted_rows: quay.io/vexxhost/nova:zed
   nova_cell_setup_init: quay.io/vexxhost/heat:zed
@@ -156,7 +156,7 @@
   octavia_worker: quay.io/vexxhost/octavia:zed
   openvswitch_db_server: quay.io/vexxhost/openvswitch:2.17.3
   openvswitch_vswitchd: quay.io/vexxhost/openvswitch:2.17.3
-  pause: k8s.gcr.io/pause:3.5
+  pause: registry.k8s.io/pause:3.8
   percona_xtradb_cluster_haproxy: docker.io/percona/percona-xtradb-cluster-operator:1.10.0-haproxy
   percona_xtradb_cluster_operator: docker.io/percona/percona-xtradb-cluster-operator:1.10.0
   percona_xtradb_cluster: docker.io/percona/percona-xtradb-cluster:5.7.39-31.61
@@ -169,7 +169,7 @@
   prometheus_mysqld_exporter: quay.io/prometheus/mysqld-exporter:v0.14.0
   prometheus_node_exporter: quay.io/prometheus/node-exporter:v1.3.1
   prometheus_openstack_exporter: ghcr.io/openstack-exporter/openstack-exporter:1.6.0
-  prometheus_operator_kube_webhook_certgen: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.3.0
+  prometheus_operator_kube_webhook_certgen: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.3.0
   prometheus_operator: quay.io/prometheus-operator/prometheus-operator:v0.60.1
   prometheus_pushgateway: docker.io/prom/pushgateway:v1.4.2
   prometheus: quay.io/prometheus/prometheus:v2.39.1
diff --git a/roles/kubernetes/README.md b/roles/kubernetes/README.md
deleted file mode 100644
index 8aa03c6..0000000
--- a/roles/kubernetes/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# `kubernetes`
diff --git a/roles/kubernetes/defaults/main.yml b/roles/kubernetes/defaults/main.yml
deleted file mode 100644
index 02ece9b..0000000
--- a/roles/kubernetes/defaults/main.yml
+++ /dev/null
@@ -1,82 +0,0 @@
----
-# .. vim: foldmarker=[[[,]]]:foldmethod=marker
-
-# .. Copyright (C) 2022 VEXXHOST, Inc.
-# .. SPDX-License-Identifier: Apache-2.0
-
-# Default variables
-# =================
-
-# .. contents:: Sections
-#    :local:
-
-
-# .. envvar:: kubernetes_repo_url [[[
-#
-# Kubernetes repository URL
-kubernetes_repo_url: "{{ _kubernetes_upstream_apt_repository }}"
-
-                                                                   # ]]]
-# .. envvar:: kubernetes_version [[[
-#
-# Kubernetes version
-kubernetes_version: 1.22.17
-kubernetes_cri_tools_version: 1.25.0
-
-                                                                   # ]]]
-# .. envvar:: kubernetes_cri_socket [[[
-#
-# CRI socket path
-kubernetes_cri_socket: /run/containerd/containerd.sock
-
-                                                                   # ]]]
-# .. envvar:: kubernetes_kernel_modules [[[
-#
-# List of kernel modules to be automatically loaded
-kubernetes_kernel_modules:
-  - br_netfilter
-
-                                                                   # ]]]
-# .. envvar:: kubernetes_sysctls [[[
-#
-# List of ``sysctl`` parameters to set
-kubernetes_sysctls:
-  - name: net.ipv4.ip_forward
-    value: 1
-  - name: net.bridge.bridge-nf-call-iptables
-    value: 1
-  - name: net.bridge.bridge-nf-call-ip6tables
-    value: 1
-  - name: net.ipv4.conf.all.rp_filter
-    value: 0
-
-                                                                   # ]]]
-# .. envvar:: kubernetes_control_plane_group [[[
-#
-# Name of Ansible group containing all control-plane nodes
-kubernetes_control_plane_group: controllers
-
-                                                                   # ]]]
-# .. envvar:: kubernetes_control_plane_labels [[[
-#
-# Labels to apply for all control plane nodes
-kubernetes_control_plane_labels:
-  openstack-control-plane: enabled
-  openvswitch: enabled
-
-                                                                   # ]]]
-# .. envvar:: kubernetes_compute_node_labels [[[
-#
-# Labels to apply for all compute nodes
-kubernetes_compute_node_labels:
-  openstack-compute-node: enabled
-  openvswitch: enabled
-
-                                                                   # ]]]
-
-# Node IP address
-kubernetes_node_ip: "{{ ansible_default_ipv4.address }}"
-
-# Allow usage of swap memory for the Kubelet (Do not enable this unless you
-# know what you are doing).
-kubernetes_allow_unsafe_swap: false
diff --git a/roles/kubernetes/files/apt-key.gpg b/roles/kubernetes/files/apt-key.gpg
deleted file mode 100644
index 0f47814..0000000
--- a/roles/kubernetes/files/apt-key.gpg
+++ /dev/null
Binary files differ
diff --git a/roles/kubernetes/files/noswap.service b/roles/kubernetes/files/noswap.service
deleted file mode 100644
index 30fedd4..0000000
--- a/roles/kubernetes/files/noswap.service
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-Description=Disable swap
-Before=kubelet.service
-After=local-fs.target
-
-[Service]
-Type=oneshot
-User=root
-ExecStart=/sbin/swapoff -a
-
-[Install]
-WantedBy=default.target
diff --git a/roles/kubernetes/tasks/bootstrap-cluster.yml b/roles/kubernetes/tasks/bootstrap-cluster.yml
deleted file mode 100644
index ddc8528..0000000
--- a/roles/kubernetes/tasks/bootstrap-cluster.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- name: Determine node to use for bootstrapping cluster
-  block:
-    - name: Check if any control plane is bootstrapped
-      ansible.builtin.stat:
-        path: /etc/kubernetes/admin.conf
-      register: _kubernetes_stat
-      loop: "{{ groups[kubernetes_control_plane_group] }}"
-      delegate_to: "{{ item }}"
-      delegate_facts: true
-
-- name: Pick node from pre-existing cluster
-  ansible.builtin.set_fact:
-    _kubernetes_bootstrap_node: "{{ _kubernetes_stat.results | selectattr('stat.exists', 'equalto', true) | map(attribute='item') | first }}"
-  when: _kubernetes_stat.results | selectattr('stat.exists', 'equalto', true) | length > 0
-
-- name: Select first node to initialize cluster
-  ansible.builtin.set_fact:
-    _kubernetes_bootstrap_node: "{{ groups[kubernetes_control_plane_group] | first }}"
-  when: _kubernetes_stat.results | selectattr('stat.exists', 'equalto', true) | length == 0
-
-- name: Print selected bootstrap node
-  ansible.builtin.debug:
-    msg: "{{ _kubernetes_bootstrap_node }}"
-
-- name: Upload cluster configuration for bootstrap node
-  ansible.builtin.template:
-    src: kubeadm.yaml.j2
-    dest: /etc/kubernetes/kubeadm.yaml
-    owner: root
-    group: root
-    mode: "0640"
-  when: inventory_hostname == _kubernetes_bootstrap_node
-
-- name: Initialize cluster
-  throttle: 1
-  ansible.builtin.shell: |
-    kubeadm init --config /etc/kubernetes/kubeadm.yaml --upload-certs \
-                 --ignore-preflight-errors=DirAvailable--etc-kubernetes-manifests{% if kubernetes_allow_unsafe_swap %},Swap{% endif %}
-  args:
-    creates: /etc/kubernetes/admin.conf
-  environment:
-    PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
-  when: inventory_hostname == _kubernetes_bootstrap_node
-
-- name: Join cluster
-  ansible.builtin.include_tasks: join-cluster.yml
-  when: inventory_hostname != _kubernetes_bootstrap_node
diff --git a/roles/kubernetes/tasks/control-plane.yml b/roles/kubernetes/tasks/control-plane.yml
deleted file mode 100644
index 10f45b0..0000000
--- a/roles/kubernetes/tasks/control-plane.yml
+++ /dev/null
@@ -1,118 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- name: Upload configuration for Keepalived
-  when: kubernetes_keepalived_interface is defined
-  block:
-    - name: Create folder
-      ansible.builtin.file:
-        dest: /etc/keepalived
-        state: directory
-        owner: root
-        group: root
-        mode: "0755"
-    - name: Upload configuration
-      ansible.builtin.template:
-        src: keepalived.conf.j2
-        dest: /etc/keepalived/keepalived.conf
-        owner: root
-        group: root
-        mode: "0644"
-    - name: Upload health check
-      ansible.builtin.template:
-        src: check_apiserver.sh.j2
-        dest: /etc/keepalived/check_apiserver.sh
-        mode: "0755"
-    - name: Upload Kubernetes manifest
-      ansible.builtin.template:
-        src: keepalived.yaml.j2
-        dest: /etc/kubernetes/manifests/keepalived.yaml
-        owner: root
-        group: root
-        mode: "0644"
-
-- name: Upload configuration for HAproxy
-  when: kubernetes_keepalived_interface is defined
-  block:
-    - name: Create folder
-      ansible.builtin.file:
-        dest: /etc/haproxy
-        state: directory
-        owner: root
-        group: root
-        mode: "0755"
-    - name: Upload configuration
-      ansible.builtin.template:
-        src: haproxy.cfg.j2
-        dest: /etc/haproxy/haproxy.cfg
-        owner: root
-        group: root
-        mode: "0644"
-    - name: Upload Kubernetes manifest
-      ansible.builtin.template:
-        src: haproxy.yaml.j2
-        dest: /etc/kubernetes/manifests/haproxy.yaml
-        owner: root
-        group: root
-        mode: "0644"
-
-- name: Bootstrap cluster
-  ansible.builtin.include_tasks: bootstrap-cluster.yml
-
-- name: Create folder for admin configuration
-  ansible.builtin.file:
-    path: /root/.kube
-    state: directory
-    owner: root
-    group: root
-    mode: "0750"
-
-- name: Creating a symlink for admin configuration file
-  ansible.builtin.file:
-    src: /etc/kubernetes/admin.conf
-    dest: /root/.kube/config
-    state: link
-    force: true
-
-- name: Add bash autocomplete for kubectl
-  ansible.builtin.lineinfile:
-    path: /root/.bashrc
-    line: 'source <(kubectl completion bash)'
-    insertbefore: EOF
-
-- name: Install PIP
-  ansible.builtin.apt:
-    name: python3-pip
-    install_recommends: false
-
-- name: Install Kubernetes python package
-  ansible.builtin.pip:
-    name: kubernetes
-
-- name: Allow workloads on control plane nodes
-  run_once: true
-  ansible.builtin.shell: |
-    kubectl taint nodes --all node-role.kubernetes.io/master-
-  failed_when: false
-  changed_when: false
-
-- name: Add labels to control plane nodes
-  kubernetes.core.k8s:
-    state: patched
-    kind: Node
-    name: "{{ inventory_hostname_short }}"
-    definition:
-      metadata:
-        labels:
-          openstack-control-plane: enabled
diff --git a/roles/kubernetes/tasks/join-cluster.yml b/roles/kubernetes/tasks/join-cluster.yml
deleted file mode 100644
index 3c3661a..0000000
--- a/roles/kubernetes/tasks/join-cluster.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- name: Check if the node is already part of the cluster
-  ansible.builtin.stat:
-    path: /etc/kubernetes/kubelet.conf
-  register: _stat_etc_kubernetes_kubelet_conf
-
-- name: Generate control-plane certificates for joining cluster
-  run_once: true
-  delegate_to: "{{ _kubernetes_bootstrap_node | default(groups[kubernetes_control_plane_group][0]) }}"
-  ansible.builtin.command: kubeadm init phase upload-certs --upload-certs
-  changed_when: false
-  register: _kubeadm_init_upload_certs
-  when:
-    - not _stat_etc_kubernetes_kubelet_conf.stat.exists
-    - inventory_hostname in groups[kubernetes_control_plane_group]
-
-- name: Retrieve SHA256 certificate hash
-  run_once: true
-  delegate_to: "{{ _kubernetes_bootstrap_node | default(groups[kubernetes_control_plane_group][0]) }}"
-  community.crypto.x509_certificate_info:
-    path: /etc/kubernetes/pki/ca.crt
-  register: _kubeadm_certificate_info
-  when:
-    - not _stat_etc_kubernetes_kubelet_conf.stat.exists
-
-- name: Generate token for joining cluster
-  run_once: true
-  delegate_to: "{{ _kubernetes_bootstrap_node | default(groups[kubernetes_control_plane_group][0]) }}"
-  changed_when: true
-  ansible.builtin.shell: |
-    kubeadm token create
-  register: _kubeadm_token_create
-  when:
-    - not _stat_etc_kubernetes_kubelet_conf.stat.exists
-
-- name: Upload kubeadm configuration
-  ansible.builtin.template:
-    src: kubeadm.yaml.j2
-    dest: /etc/kubernetes/kubeadm.yaml
-    owner: root
-    group: root
-    mode: "0640"
-  when:
-    - not _stat_etc_kubernetes_kubelet_conf.stat.exists
-
-- name: Join cluster
-  ansible.builtin.shell: |
-    kubeadm join --config /etc/kubernetes/kubeadm.yaml \
-                 --ignore-preflight-errors=DirAvailable--etc-kubernetes-manifests{% if kubernetes_allow_unsafe_swap %},Swap{% endif %}
-  environment:
-    PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
-  args:
-    creates: /etc/kubernetes/kubelet.conf
diff --git a/roles/kubernetes/tasks/main.yml b/roles/kubernetes/tasks/main.yml
deleted file mode 100644
index e6d4356..0000000
--- a/roles/kubernetes/tasks/main.yml
+++ /dev/null
@@ -1,144 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- name: Add repository keys
-  ansible.builtin.copy:
-    src: apt-key.gpg
-    dest: /usr/share/keyrings/kubernetes-archive-keyring.gpg
-    owner: root
-    group: root
-    mode: "0644"
-  when:
-    - kubernetes_repo_url == _kubernetes_upstream_apt_repository
-
-- name: Add repository
-  ansible.builtin.apt_repository:
-    repo:
-      deb
-      {% if kubernetes_repo_url == _kubernetes_upstream_apt_repository %}[signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg]{% endif %}
-      {{ kubernetes_repo_url }}
-      kubernetes-xenial
-      main
-    state: present
-
-- name: Setup version pins
-  ansible.builtin.template:
-    src: apt-preferences.j2
-    dest: /etc/apt/preferences.d/kubernetes
-    mode: "0644"
-
-- name: Install packages
-  ansible.builtin.apt:
-    name:
-      - "cri-tools={{ kubernetes_cri_tools_version }}-00"
-      - "kubeadm={{ kubernetes_version }}-00"
-      - "kubectl={{ kubernetes_version }}-00"
-      - "kubelet={{ kubernetes_version }}-00"
-    state: present
-
-- name: Enable kernel modules on-boot
-  ansible.builtin.template:
-    src: modules-load.conf.j2
-    dest: /etc/modules-load.d/k8s.conf
-    owner: root
-    group: root
-    mode: "0644"
-
-- name: Enable kernel modules in runtime
-  community.general.modprobe:
-    name: "{{ item }}"
-    state: present
-  loop: "{{ kubernetes_kernel_modules }}"
-
-- name: Configure sysctl values
-  ansible.posix.sysctl:
-    name: "{{ item.name }}"
-    value: "{{ item.value }}"
-    state: present
-  loop: "{{ kubernetes_sysctls }}"
-
-- name: Disable swap on the host
-  when: not kubernetes_allow_unsafe_swap | bool
-  block:
-    - name: Check swap status
-      ansible.builtin.command: /sbin/swapon -s
-      changed_when: false
-      register: _swapon
-
-    - name: Disable swap
-      ansible.builtin.command: /sbin/swapoff -a
-      changed_when: true
-      ignore_errors: "{{ ansible_check_mode }}"
-      when:
-        - _swapon.stdout
-
-    - name: Remove swapfile from /etc/fstab
-      ansible.posix.mount:
-        name: "{{ item }}"
-        fstype: swap
-        state: absent
-      with_items:
-        - swap
-        - none
-
-    - name: Create noswap systemd service config file
-      ansible.builtin.copy:
-        src: noswap.service
-        dest: /etc/systemd/system/noswap.service
-        owner: root
-        group: root
-        mode: "0644"
-      notify: Enable noswap service
-
-- name: Configure short hostname
-  ansible.builtin.hostname:
-    name: "{{ inventory_hostname_short }}"
-
-- name: Ensure hostname inside hosts file
-  ansible.builtin.lineinfile:
-    path: /etc/hosts
-    regexp: '^127\.0\.1\.1'
-    line: 127.0.1.1 {{ inventory_hostname }} {{ inventory_hostname_short }}
-
-- name: Setup control plane
-  when: inventory_hostname in groups[kubernetes_control_plane_group]
-  ansible.builtin.include_tasks: control-plane.yml
-
-- name: Setup nodes
-  when: inventory_hostname not in groups[kubernetes_control_plane_group]
-  ansible.builtin.include_tasks: nodes.yml
-
-- name: Add labels to control plane nodes
-  delegate_to: "{{ groups[kubernetes_control_plane_group][0] }}"
-  kubernetes.core.k8s:
-    state: patched
-    kind: Node
-    name: "{{ inventory_hostname_short }}"
-    definition:
-      metadata:
-        labels: "{{ kubernetes_control_plane_labels }}"
-  when:
-    - inventory_hostname in groups['controllers']
-
-- name: Add labels to compute nodes
-  delegate_to: "{{ groups[kubernetes_control_plane_group][0] }}"
-  kubernetes.core.k8s:
-    state: patched
-    kind: Node
-    name: "{{ inventory_hostname_short }}"
-    definition:
-      metadata:
-        labels: "{{ kubernetes_compute_node_labels }}"
-  when:
-    - inventory_hostname in groups['computes']
diff --git a/roles/kubernetes/tasks/nodes.yml b/roles/kubernetes/tasks/nodes.yml
deleted file mode 100644
index 5b4f688..0000000
--- a/roles/kubernetes/tasks/nodes.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-- name: Check if Kubernetes is already deployed
-  ansible.builtin.stat:
-    path: /etc/kubernetes/kubelet.conf
-  register: _kubernetes_kubelet
-
-- name: Join cluster
-  when: not _kubernetes_kubelet.stat.exists
-  ansible.builtin.include_tasks: join-cluster.yml
diff --git a/roles/kubernetes/templates/apt-preferences.j2 b/roles/kubernetes/templates/apt-preferences.j2
deleted file mode 100644
index aad283b..0000000
--- a/roles/kubernetes/templates/apt-preferences.j2
+++ /dev/null
@@ -1,11 +0,0 @@
-Package: kubectl
-Pin: version {{ kubernetes_version }}-00
-Pin-Priority: 1000
-
-Package: kubeadm
-Pin: version {{ kubernetes_version }}-00
-Pin-Priority: 1000
-
-Package: kubelet
-Pin: version {{ kubernetes_version }}-00
-Pin-Priority: 1000
diff --git a/roles/kubernetes/templates/check_apiserver.sh.j2 b/roles/kubernetes/templates/check_apiserver.sh.j2
deleted file mode 100644
index 5d81310..0000000
--- a/roles/kubernetes/templates/check_apiserver.sh.j2
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/sh
-
-errorExit() {
-    echo "*** $*" 1>&2
-    exit 1
-}
-
-curl --silent --max-time 2 --insecure https://localhost:16443/ -o /dev/null || errorExit "Error GET https://localhost:16443/"
-if ip addr | grep -q {{ kubernetes_keepalived_vip }}; then
-    curl --silent --max-time 2 --insecure https://{{ kubernetes_keepalived_vip }}:6443/ -o /dev/null || errorExit "Error GET https://{{ kubernetes_keepalived_vip }}:6443/"
-fi
diff --git a/roles/kubernetes/templates/crictl.yaml.j2 b/roles/kubernetes/templates/crictl.yaml.j2
deleted file mode 100644
index a5e8dc2..0000000
--- a/roles/kubernetes/templates/crictl.yaml.j2
+++ /dev/null
@@ -1,4 +0,0 @@
-runtime-endpoint: unix://{{ kubernetes_cri_socket }}
-image-endpoint: unix://{{ kubernetes_cri_socket }}
-timeout: 30
-debug: false
diff --git a/roles/kubernetes/templates/haproxy.cfg.j2 b/roles/kubernetes/templates/haproxy.cfg.j2
deleted file mode 100644
index ef66a48..0000000
--- a/roles/kubernetes/templates/haproxy.cfg.j2
+++ /dev/null
@@ -1,51 +0,0 @@
-# /etc/haproxy/haproxy.cfg
-#---------------------------------------------------------------------
-# Global settings
-#---------------------------------------------------------------------
-global
-    log /dev/log local0
-    log /dev/log local1 notice
-    daemon
-
-#---------------------------------------------------------------------
-# common defaults that all the 'listen' and 'backend' sections will
-# use if not designated in their block
-#---------------------------------------------------------------------
-defaults
-    mode                    http
-    log                     global
-    option                  httplog
-    option                  dontlognull
-    option http-server-close
-    option forwardfor       except 127.0.0.0/8
-    option                  redispatch
-    retries                 1
-    timeout http-request    10s
-    timeout queue           20s
-    timeout connect         5s
-    timeout client          20s
-    timeout server          20s
-    timeout http-keep-alive 10s
-    timeout check           10s
-
-#---------------------------------------------------------------------
-# apiserver frontend which proxys to the masters
-#---------------------------------------------------------------------
-frontend apiserver
-    bind *:6443
-    mode tcp
-    option tcplog
-    default_backend apiserver
-
-#---------------------------------------------------------------------
-# round robin balancing for apiserver
-#---------------------------------------------------------------------
-backend apiserver
-    option httpchk GET /healthz
-    http-check expect status 200
-    mode tcp
-    option ssl-hello-chk
-    balance     roundrobin
-{% for host in groups[kubernetes_control_plane_group] %}
-        server {{ host }} {{ hostvars[host]['kubernetes_node_ip'] | default(hostvars[host]['ansible_default_ipv4']['address']) }}:16443 check
-{% endfor %}
diff --git a/roles/kubernetes/templates/haproxy.yaml.j2 b/roles/kubernetes/templates/haproxy.yaml.j2
deleted file mode 100644
index cc753a9..0000000
--- a/roles/kubernetes/templates/haproxy.yaml.j2
+++ /dev/null
@@ -1,27 +0,0 @@
-apiVersion: v1
-kind: Pod
-metadata:
-  name: haproxy
-  namespace: kube-system
-spec:
-  containers:
-    - name: haproxy
-      image: "{{ atmosphere_images['haproxy'] | vexxhost.kubernetes.docker_image('ref') }}"
-      livenessProbe:
-        failureThreshold: 8
-        httpGet:
-          host: localhost
-          path: /healthz
-          port: 6443
-          scheme: HTTPS
-      volumeMounts:
-        - mountPath: /usr/local/etc/haproxy/haproxy.cfg
-          name: haproxyconf
-          readOnly: true
-  hostNetwork: true
-  volumes:
-    - hostPath:
-        path: /etc/haproxy/haproxy.cfg
-        type: FileOrCreate
-      name: haproxyconf
-status: {}
diff --git a/roles/kubernetes/templates/keepalived.conf.j2 b/roles/kubernetes/templates/keepalived.conf.j2
deleted file mode 100644
index 7efe465..0000000
--- a/roles/kubernetes/templates/keepalived.conf.j2
+++ /dev/null
@@ -1,25 +0,0 @@
-global_defs {
-    router_id LVS_DEVEL
-}
-
-vrrp_script check_apiserver {
-    script "/etc/keepalived/check_apiserver.sh"
-    interval 3
-    weight -2
-    fall 10
-    rise 2
-}
-
-vrrp_instance kubernetes {
-    state MASTER
-    interface {{ kubernetes_keepalived_interface }}
-    virtual_router_id {{ kubernetes_keepalived_vrid }}
-
-    virtual_ipaddress {
-        {{ kubernetes_keepalived_vip }}
-    }
-
-    track_script {
-        check_apiserver
-    }
-}
diff --git a/roles/kubernetes/templates/keepalived.yaml.j2 b/roles/kubernetes/templates/keepalived.yaml.j2
deleted file mode 100644
index a2b579d..0000000
--- a/roles/kubernetes/templates/keepalived.yaml.j2
+++ /dev/null
@@ -1,32 +0,0 @@
-apiVersion: v1
-kind: Pod
-metadata:
-  creationTimestamp: null
-  name: keepalived
-  namespace: kube-system
-spec:
-  containers:
-    - name: keepalived
-      image: "{{ atmosphere_images['keepalived'] | vexxhost.kubernetes.docker_image('ref') }}"
-      command: ["keepalived", "-f", "/etc/keepalived/keepalived.conf", "--dont-fork", "--log-console", "--log-detail", "--dump-conf"]
-      resources: {}
-      securityContext:
-        capabilities:
-          add:
-            - NET_ADMIN
-            - NET_BROADCAST
-            - NET_RAW
-      volumeMounts:
-        - mountPath: /etc/keepalived/keepalived.conf
-          name: config
-        - mountPath: /etc/keepalived/check_apiserver.sh
-          name: check
-  hostNetwork: true
-  volumes:
-    - hostPath:
-        path: /etc/keepalived/keepalived.conf
-      name: config
-    - hostPath:
-        path: /etc/keepalived/check_apiserver.sh
-      name: check
-status: {}
diff --git a/roles/kubernetes/templates/kubeadm.yaml.j2 b/roles/kubernetes/templates/kubeadm.yaml.j2
deleted file mode 100644
index f77c223..0000000
--- a/roles/kubernetes/templates/kubeadm.yaml.j2
+++ /dev/null
@@ -1,64 +0,0 @@
----
-apiVersion: kubeadm.k8s.io/v1beta3
-kind: InitConfiguration
-localAPIEndpoint:
-  advertiseAddress: "{{ kubernetes_node_ip }}"
-  bindPort: 16443
-nodeRegistration:
-  kubeletExtraArgs:
-{% if kubernetes_allow_unsafe_swap %}
-    fail-swap-on: "false"
-{% endif %}
-    cgroups-per-qos: "false"
-    enforce-node-allocatable: ""
-    node-ip: "{{ kubernetes_node_ip }}"
-    container-runtime: "remote"
-    container-runtime-endpoint: "{{ kubernetes_cri_socket }}"
----
-apiVersion: kubeadm.k8s.io/v1beta2
-kind: JoinConfiguration
-nodeRegistration:
-  kubeletExtraArgs:
-{% if kubernetes_allow_unsafe_swap %}
-    fail-swap-on: "false"
-{% endif %}
-    cgroups-per-qos: "false"
-    enforce-node-allocatable: ""
-    node-ip: "{{ kubernetes_node_ip }}"
-    container-runtime: "remote"
-    container-runtime-endpoint: "{{ kubernetes_cri_socket }}"
-{% if (_kubernetes_bootstrap_node is not defined) or (_kubernetes_bootstrap_node is defined and inventory_hostname != _kubernetes_bootstrap_node) %}
-discovery:
-  bootstrapToken:
-    token: "{{ _kubeadm_token_create.stdout | trim }}"
-    apiServerEndpoint: "{{ kubernetes_hostname }}:6443"
-    caCertHashes: ["sha256:{{ _kubeadm_certificate_info.public_key_fingerprints.sha256 | replace(':', '') }}"]
-{% if inventory_hostname in groups[kubernetes_control_plane_group] %}
-controlPlane:
-  localAPIEndpoint:
-    advertiseAddress: "{{ kubernetes_node_ip }}"
-    bindPort: 16443
-  certificateKey: {{ _kubeadm_init_upload_certs.stdout_lines[-1] | trim }}
-{% endif %}
-{% endif %}
----
-apiVersion: kubeadm.k8s.io/v1beta3
-kind: ClusterConfiguration
-controlPlaneEndpoint: "{{ kubernetes_hostname }}:6443"
-{% if atmosphere_image_repository is defined %}
-imageRepository: "{{ atmosphere_image_repository }}"
-{% endif %}
-apiServer:
-  extraArgs:
-    oidc-username-claim: email
-{% if kubernetes_oidc_issuer_url is defined %}
-    oidc-issuer-url: {{ kubernetes_oidc_issuer_url }}
-    oidc-client-id: {{ kubernetes_oidc_client_id }}
-{% endif %}
----
-apiVersion: kubelet.config.k8s.io/v1beta1
-kind: KubeletConfiguration
----
-apiVersion: kubeproxy.config.k8s.io/v1alpha1
-kind: KubeProxyConfiguration
-metricsBindAddress: 0.0.0.0
diff --git a/roles/kubernetes/templates/modules-load.conf.j2 b/roles/kubernetes/templates/modules-load.conf.j2
deleted file mode 100644
index 351a663..0000000
--- a/roles/kubernetes/templates/modules-load.conf.j2
+++ /dev/null
@@ -1,3 +0,0 @@
-{% for kubernetes_kernel_module in kubernetes_kernel_modules %}
-{{ kubernetes_kernel_module }}
-{% endfor %}
diff --git a/roles/kubernetes/vars/main.yml b/roles/kubernetes/vars/main.yml
deleted file mode 100644
index cf153c5..0000000
--- a/roles/kubernetes/vars/main.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-_kubernetes_upstream_apt_repository: https://apt.kubernetes.io/
diff --git a/roles/kubernetes_node_labels/README.md b/roles/kubernetes_node_labels/README.md
new file mode 100644
index 0000000..51e45b1
--- /dev/null
+++ b/roles/kubernetes_node_labels/README.md
@@ -0,0 +1 @@
+# `kubernetes_node_labels`
diff --git a/roles/kubernetes_node_labels/defaults/main.yml b/roles/kubernetes_node_labels/defaults/main.yml
new file mode 100644
index 0000000..256c2e4
--- /dev/null
+++ b/roles/kubernetes_node_labels/defaults/main.yml
@@ -0,0 +1,26 @@
+# Copyright (c) 2023 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+kubernetes_node_labels_control_plane_group: controllers
+
+kubernetes_node_labels: |
+  {% set res = {} -%}
+  {% if inventory_hostname in groups['controllers'] %}
+  {%   set _ = res.update({'openstack-control-plane': 'enabled'}) %}
+  {%   set _ = res.update({'openvswitch': 'enabled'}) %}
+  {% elif inventory_hostname in groups['computes'] %}
+  {%   set _ = res.update({'openstack-compute-node': 'enabled'}) %}
+  {%   set _ = res.update({'openvswitch': 'enabled'}) %}
+  {% endif %}
+  {{ res }}
diff --git a/roles/kubernetes/meta/main.yml b/roles/kubernetes_node_labels/meta/main.yml
similarity index 84%
rename from roles/kubernetes/meta/main.yml
rename to roles/kubernetes_node_labels/meta/main.yml
index b9b6532..93ded93 100644
--- a/roles/kubernetes/meta/main.yml
+++ b/roles/kubernetes_node_labels/meta/main.yml
@@ -14,14 +14,16 @@
 
 galaxy_info:
   author: VEXXHOST, Inc.
-  description: Ansible role for Kubernetes
+  description: Ansible role for managing Kubernetes node labels
   license: Apache-2.0
   min_ansible_version: 5.5.0
   standalone: false
   platforms:
+    - name: EL
+      versions:
+        - "8"
+        - "9"
     - name: Ubuntu
       versions:
         - focal
-
-dependencies:
-  - role: defaults
+        - jammy
diff --git a/roles/kubernetes/handlers/main.yml b/roles/kubernetes_node_labels/tasks/main.yml
similarity index 67%
rename from roles/kubernetes/handlers/main.yml
rename to roles/kubernetes_node_labels/tasks/main.yml
index 4605518..774d3e8 100644
--- a/roles/kubernetes/handlers/main.yml
+++ b/roles/kubernetes_node_labels/tasks/main.yml
@@ -12,9 +12,12 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-- name: Enable noswap service
-  ansible.builtin.systemd:
-    name: noswap
-    state: started
-    enabled: true
-    daemon_reload: true
+- name: Add labels to node
+  delegate_to: "{{ groups[kubernetes_node_labels_control_plane_group][0] }}"
+  kubernetes.core.k8s:
+    state: patched
+    kind: Node
+    name: "{{ inventory_hostname_short }}"
+    definition:
+      metadata:
+        labels: "{{ kubernetes_node_labels }}"
diff --git a/tests/unit/plugins/filter/test_from_ini.py b/tests/unit/plugins/filter/test_from_ini.py
index eee4a2e..02435f4 100644
--- a/tests/unit/plugins/filter/test_from_ini.py
+++ b/tests/unit/plugins/filter/test_from_ini.py
@@ -15,9 +15,7 @@
 import textwrap
 
 import pytest
-from ansible_collections.vexxhost.atmosphere.plugins.filter.from_ini import (
-    from_ini,
-)
+from ansible_collections.vexxhost.atmosphere.plugins.filter.from_ini import from_ini
 
 
 @pytest.mark.parametrize(
@@ -28,7 +26,7 @@
                 """
                 """
             ),
-            {}
+            {},
         ),
         (
             textwrap.dedent(
@@ -47,10 +45,9 @@
                 "oslo_messaging": {
                     "transport_url": "rabbit://guest:guest@localhost:5672/",
                 },
-            }
+            },
         ),
     ],
 )
 def test_from_ini(test_input, expected):
     assert from_ini(test_input) == expected
-