Generalize kubernetes control plane resource

magnum_capi_helm driver supports only kubeadm and
there is no mechanism to use the driver with other
k8s distributions like canonical k8s [1].

To generalize the kubernets control plane, the resource
class KubeadmControlPlane is modified as K8sControlPlane.

Two new configuration parameters are added:

* api_resources - provides ability to modify the api
version and plural names for cluster api resources.
This is especially necessary for changing k8s control
plane resource api versions and plural names.
Also it will be helpful if the management cluster supports
different api version than what is hardcoded in the driver
for any cluster api resource.
Note plural is only supported for k8s control plane as we
dont see any value add to make it configurable for other
cluster api resources.

* k8s_control_plane_resource_conditions
To change the control plane resource condition check to
determine the resource status as ready. Canonical k8s
does not use etcd as k8s backend and hence the EtcdClusterHealthy
condition does not exist and the resource status is CREATE_FAILED.

[1] https://documentation.ubuntu.com/canonical-kubernetes/release-1.32/capi/
[2] https://github.com/canonical/cluster-api-k8s/tree/main

Change-Id: Iea342f8917f0b797fb3dc5810433d52841af9b55
Signed-off-by: Hemanth Nakkina <hemanth.nakkina@canonical.com>
This commit is contained in:
Hemanth Nakkina
2025-07-28 08:19:53 +05:30
parent 1bc80ffb7a
commit ccf4ebbb48
8 changed files with 105 additions and 31 deletions

View File

@@ -156,7 +156,7 @@ class CAPIMonitor(monitors.MonitorBase):
resource_name = driver_utils.get_k8s_resource_name(
self.cluster, "control-plane"
)
resource_kcp = self._k8s_client.get_kubeadm_control_plane(
resource_kcp = self._k8s_client.get_k8s_control_plane(
resource_name, namespace
)
if not resource_kcp:

View File

@@ -130,6 +130,40 @@ capi_helm_opts = [
"generated application credentials."
),
),
cfg.StrOpt(
"api_resources",
default={},
help=(
"""
Dictionary of cluster api resources to modify
api_version and plural names in string format.
"Example:"
'{
"K8sControlPlane": {
"api_version": "controlplane.cluster.x-k8s.io/v1beta1",
"plural_name": "kubeadmcontrolplanes"
},
"OpenstackCluster": {
"api_version": "infrastructure.cluster.x-k8s.io/v1beta1",
},
}'
"""
),
),
cfg.ListOpt(
"k8s_control_plane_resource_conditions",
default=[
"MachinesReady",
"Ready",
"EtcdClusterHealthy",
"ControlPlaneComponentsHealthy",
],
help=(
"List of conditions to check for kubernetes control plane "
"resource to consider as ready."
),
),
]
CONF = cfg.CONF

View File

@@ -82,7 +82,7 @@ class Driver(driver.Driver):
def _update_control_plane_nodegroup_status(self, cluster, nodegroup):
# The status of the master nodegroup is determined by the Cluster API
# control plane object
kcp = self._k8s_client.get_kubeadm_control_plane(
kcp = self._k8s_client.get_k8s_control_plane(
driver_utils.get_k8s_resource_name(cluster, "control-plane"),
driver_utils.cluster_namespace(cluster),
)
@@ -108,12 +108,7 @@ class Driver(driver.Driver):
}
kcp_ready = all(
cond in kcp_true_conditions
for cond in (
"MachinesReady",
"Ready",
"EtcdClusterHealthy",
"ControlPlaneComponentsHealthy",
)
for cond in CONF.capi_helm.k8s_control_plane_resource_conditions
)
target_replicas = kcp_spec.get("replicas")
current_replicas = kcp_status.get("replicas")

View File

@@ -12,6 +12,7 @@
import base64
import copy
import json
import os
import pathlib
import re
@@ -171,8 +172,8 @@ class Client(requests.Session):
def get_capi_openstackcluster(self, name, namespace):
return OpenstackCluster(self).fetch(name, namespace)
def get_kubeadm_control_plane(self, name, namespace):
return KubeadmControlPlane(self).fetch(name, namespace)
def get_k8s_control_plane(self, name, namespace):
return K8sControlPlane(self).fetch(name, namespace)
def get_machine_deployment(self, name, namespace):
return MachineDeployment(self).fetch(name, namespace)
@@ -201,6 +202,7 @@ class Resource:
self, "plural_name", self.kind.lower() + "s"
)
self.namespaced = getattr(self, "namespaced", True)
self.api_resources = json.loads(CONF.capi_helm.api_resources)
def prepare_path(self, name=None, namespace=None):
# Begin with either /api or /apis depending whether the api version
@@ -287,29 +289,62 @@ class Secret(Resource):
class Cluster(Resource):
api_version = "cluster.x-k8s.io/v1beta1"
api_version = (
json.loads(CONF.capi_helm.api_resources)
.get("Cluster", {})
.get("api_version", "cluster.x-k8s.io/v1beta1")
)
class OpenstackCluster(Resource):
api_version = "infrastructure.cluster.x-k8s.io/v1beta1"
api_version = (
json.loads(CONF.capi_helm.api_resources)
.get("OpenstackCluster", {})
.get("api_version", "infrastructure.cluster.x-k8s.io/v1beta1")
)
class MachineDeployment(Resource):
api_version = "cluster.x-k8s.io/v1beta1"
api_version = (
json.loads(CONF.capi_helm.api_resources)
.get("MachineDeployment", {})
.get("api_version", "cluster.x-k8s.io/v1beta1")
)
class KubeadmControlPlane(Resource):
api_version = "controlplane.cluster.x-k8s.io/v1beta1"
class K8sControlPlane(Resource):
api_version = (
json.loads(CONF.capi_helm.api_resources)
.get("K8sControlPlane", {})
.get("api_version", "controlplane.cluster.x-k8s.io/v1beta1")
)
plural_name = (
json.loads(CONF.capi_helm.api_resources)
.get("K8sControlPlane", {})
.get("plural_name", "kubeadmcontrolplanes")
)
class Machine(Resource):
api_version = "cluster.x-k8s.io/v1beta1"
api_version = (
json.loads(CONF.capi_helm.api_resources)
.get("Machine", {})
.get("api_version", "cluster.x-k8s.io/v1beta1")
)
class Manifests(Resource):
api_version = "addons.stackhpc.com/v1alpha1"
api_version = (
json.loads(CONF.capi_helm.api_resources)
.get("Manifests", {})
.get("api_version", "addons.stackhpc.com/v1alpha1")
)
plural_name = "manifests"
class HelmRelease(Resource):
api_version = "addons.stackhpc.com/v1alpha1"
api_version = (
json.loads(CONF.capi_helm.api_resources)
.get("HelmRelease", {})
.get("api_version", "addons.stackhpc.com/v1alpha1")
)

View File

@@ -55,7 +55,7 @@ class TestCAPIMonitor(base.DbTestCase):
"ready": True,
}
}
self.mock_k8s.get_kubeadm_control_plane.return_value = copy.deepcopy(
self.mock_k8s.get_k8s_control_plane.return_value = copy.deepcopy(
ready_state
)
self.mock_k8s.get_machine_deployment.return_value = copy.deepcopy(
@@ -193,7 +193,7 @@ class TestCAPIMonitor(base.DbTestCase):
]
}
}
self.mock_k8s.get_kubeadm_control_plane.return_value = cp_state
self.mock_k8s.get_k8s_control_plane.return_value = cp_state
self.monitor.poll_health_status()
self.assertEqual(
@@ -264,7 +264,7 @@ class TestCAPIMonitor(base.DbTestCase):
def test_all_missing(self):
self.mock_k8s.get_capi_cluster.return_value = None
self.mock_k8s.get_capi_openstackcluster.return_value = None
self.mock_k8s.get_kubeadm_control_plane.return_value = None
self.mock_k8s.get_k8s_control_plane.return_value = None
self.mock_k8s.get_machine_deployment.return_value = None
self.monitor.poll_health_status()

View File

@@ -413,13 +413,13 @@ class ClusterAPIDriverTest(base.DbTestCase):
mock_load.return_value = mock_client
nodegroup = mock.MagicMock()
nodegroup.name = "masters"
mock_client.get_kubeadm_control_plane.return_value = None
mock_client.get_k8s_control_plane.return_value = None
self.driver._update_control_plane_nodegroup_status(
self.cluster_obj, nodegroup
)
mock_client.get_kubeadm_control_plane.assert_called_once_with(
mock_client.get_k8s_control_plane.assert_called_once_with(
"cluster-example-a-111111111111-control-plane",
"magnum-fakeproject",
)
@@ -455,13 +455,13 @@ class ClusterAPIDriverTest(base.DbTestCase):
"readyReplicas": 3,
},
}
mock_client.get_kubeadm_control_plane.return_value = kcp
mock_client.get_k8s_control_plane.return_value = kcp
self.driver._update_control_plane_nodegroup_status(
self.cluster_obj, nodegroup
)
mock_client.get_kubeadm_control_plane.assert_called_once_with(
mock_client.get_k8s_control_plane.assert_called_once_with(
"cluster-example-a-111111111111-control-plane",
"magnum-fakeproject",
)
@@ -497,13 +497,13 @@ class ClusterAPIDriverTest(base.DbTestCase):
"readyReplicas": 2,
},
}
mock_client.get_kubeadm_control_plane.return_value = kcp
mock_client.get_k8s_control_plane.return_value = kcp
self.driver._update_control_plane_nodegroup_status(
self.cluster_obj, nodegroup
)
mock_client.get_kubeadm_control_plane.assert_called_once_with(
mock_client.get_k8s_control_plane.assert_called_once_with(
"cluster-example-a-111111111111-control-plane",
"magnum-fakeproject",
)
@@ -539,13 +539,13 @@ class ClusterAPIDriverTest(base.DbTestCase):
"readyReplicas": 3,
},
}
mock_client.get_kubeadm_control_plane.return_value = kcp
mock_client.get_k8s_control_plane.return_value = kcp
self.driver._update_control_plane_nodegroup_status(
self.cluster_obj, nodegroup
)
mock_client.get_kubeadm_control_plane.assert_called_once_with(
mock_client.get_k8s_control_plane.assert_called_once_with(
"cluster-example-a-111111111111-control-plane",
"magnum-fakeproject",
)

View File

@@ -301,14 +301,14 @@ class TestKubernetesClient(base.TestCase):
)
@mock.patch.object(requests.Session, "request")
def test_get_kubeadm_control_plane_found(self, mock_request):
def test_get_k8s_control_plane_found(self, mock_request):
client = kubernetes.Client(TEST_KUBECONFIG)
mock_response = mock.MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = "mock_json"
mock_request.return_value = mock_response
cluster = client.get_kubeadm_control_plane("name", "ns1")
cluster = client.get_k8s_control_plane("name", "ns1")
mock_request.assert_called_once_with(
"GET",

View File

@@ -0,0 +1,10 @@
---
features:
- |
Adds new configuration parameters to update api_version and plural names
of k8s resources related to Cluster API. To be specific, the resources
are Cluster, OpenstackCluster, MachineDeployment, K8sControlPlane,
Machine, Manifests, HelmRelease.
Adds new configuration parameter to specify list of conditions to check
in kubernetes control plane resource to consider resource as ready.