Skip to content

Commit bb3a3ce

Browse files
committed
refactor: make t8s HelmRelease identifiers instance attributes
Signed-off-by: Chris Werner Rau <cwrau@cwrau.info>
1 parent 779dff7 commit bb3a3ce

2 files changed

Lines changed: 112 additions & 48 deletions

File tree

Tests/kaas/plugin/plugin_t8s.py

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,14 @@
2222
HR_VERSION = "v2"
2323
HR_PLURAL = "helmreleases"
2424

25-
# All of these are mandated by the ValidatingAdmissionPolicy on the management cluster
26-
# and cannot be changed without updating both the VAP and the RBAC.
27-
HR_NAME = "scs-kaas-certification"
28-
HR_NAMESPACE = "scs-kaas-certification"
29-
HR_KUBECONFIG_SECRET = "scs-kaas-certification-kubeconfig"
30-
HR_CHART_REF = {
31-
"kind": "HelmChart",
32-
"name": "scs-kaas-certification",
33-
"namespace": "flux-system",
34-
}
35-
HR_VALUES_FIXED = {
36-
"cloud": "bfe2-prod",
37-
"controlPlane": {"hosted": True},
38-
"metadata": {
39-
"customerID": 1111,
40-
"customerName": "teuto.net Netzdienste GmbH",
41-
"friendlyName": "scs-kaas-certification",
42-
"serviceLevelAgreement": "None",
43-
},
44-
"nodePools": {
45-
"pool-0": {"flavor": "standard.2.1905", "replicas": 2},
46-
},
47-
}
48-
4925

5026
class PluginT8s(KubernetesClusterPlugin):
5127
"""
5228
Plugin to provision Kubernetes clusters on the t8s (teuto k8s) management cluster
5329
via a Flux HelmRelease.
5430
5531
Creates a HelmRelease on mgmt-bfe2-prod and waits for the resulting cluster's
56-
kubeconfig to appear in the secret scs-kaas-certification-kubeconfig.
32+
kubeconfig to appear in the secret '<name>-kubeconfig'.
5733
5834
Expected config keys:
5935
kubernetesVersion: '1.35' (major.minor)
@@ -62,13 +38,53 @@ class PluginT8s(KubernetesClusterPlugin):
6238
kubeconfig: t8s-kubeconfig.yaml (management cluster kubeconfig template)
6339
secrets:
6440
token: '{{ clouds_conf.teuto_mgmt_token }}'
41+
name: 'scs-kaas-certification' (optional, defaults to 'scs-kaas-certification';
42+
the kubeconfig secret name is derived as '<name>-kubeconfig')
43+
cloud: 'bfe2-prod' (optional, defaults to 'bfe2-prod')
44+
controlPlaneHosted: true (optional, defaults to true)
45+
nodePools: (optional, defaults to a single 'pool-0')
46+
pool-0:
47+
flavor: 'standard.2.1905'
48+
replicas: 2
49+
metadata: (optional; individual keys default as shown)
50+
customerID: 1111
51+
customerName: 'teuto.net Netzdienste GmbH'
52+
friendlyName: 'scs-kaas-certification'
53+
serviceLevelAgreement: 'None'
6554
"""
6655

6756
def __init__(self, config: dict[str, Any], basepath: str = ".", cwd: str = ".") -> None:
6857
self.basepath = basepath
6958
self.cwd = cwd
7059
self.env = Environment()
7160

61+
# All of these are mandated by the ValidatingAdmissionPolicy on the management cluster
62+
# and cannot be changed without updating both the VAP and the RBAC. They are instance
63+
# (rather than module-level) attributes because, in principle, different certification
64+
# targets could be configured with different values here.
65+
self.hr_name = config.get("name", "scs-kaas-certification")
66+
self.hr_namespace = "scs-kaas-certification"
67+
self.hr_kubeconfig_secret = f"{self.hr_name}-kubeconfig"
68+
self.hr_chart_ref: dict[str, str] = {
69+
"kind": "HelmChart",
70+
"name": "scs-kaas-certification",
71+
"namespace": "flux-system",
72+
}
73+
metadata_config: dict[str, Any] = config.get("metadata", {})
74+
self.hr_values_fixed: dict[str, Any] = {
75+
"cloud": config.get("cloud", "bfe2-prod"),
76+
"controlPlane": {"hosted": config.get("controlPlaneHosted", True)},
77+
"metadata": {
78+
"customerID": metadata_config.get("customerID", 1111),
79+
"customerName": metadata_config.get("customerName", "teuto.net Netzdienste GmbH"),
80+
"friendlyName": metadata_config.get("friendlyName", "scs-kaas-certification"),
81+
"serviceLevelAgreement": metadata_config.get("serviceLevelAgreement", "None"),
82+
},
83+
"nodePools": config.get("nodePools", {
84+
"pool-0": {"flavor": "standard.2.1905", "replicas": 2},
85+
}),
86+
}
87+
7288
fn: str = config["templates"]["kubeconfig"]
7389
with open(os.path.join(basepath, fn), "r") as f:
7490
self.kubeconfig_template: Template = self.env.from_string(f.read())
@@ -91,12 +107,12 @@ def _build_helmrelease(self) -> dict[str, Any]:
91107
return {
92108
"apiVersion": f"{HR_GROUP}/{HR_VERSION}",
93109
"kind": "HelmRelease",
94-
"metadata": {"name": HR_NAME, "namespace": HR_NAMESPACE},
110+
"metadata": {"name": self.hr_name, "namespace": self.hr_namespace},
95111
"spec": {
96-
"chartRef": HR_CHART_REF,
112+
"chartRef": self.hr_chart_ref,
97113
"driftDetection": {"mode": "enabled"},
98114
"interval": "1m",
99-
"values": {**HR_VALUES_FIXED, "version": self.k8s_version},
115+
"values": {**self.hr_values_fixed, "version": self.k8s_version},
100116
},
101117
}
102118

@@ -105,17 +121,17 @@ def _delete_helmrelease(self, co_api: CustomObjectsApi) -> None:
105121
co_api.delete_namespaced_custom_object(
106122
HR_GROUP,
107123
HR_VERSION,
108-
HR_NAMESPACE,
124+
self.hr_namespace,
109125
HR_PLURAL,
110-
HR_NAME,
126+
self.hr_name,
111127
)
112128
except ApiException as e:
113129
if e.status == 404:
114-
logger.debug(f"HelmRelease {HR_NAME} not present, nothing to delete")
130+
logger.debug(f"HelmRelease {self.hr_name} not present, nothing to delete")
115131
return
116132
raise
117133
# Give Flux time to begin deletion before we try to recreate
118-
logger.debug(f"HelmRelease {HR_NAME} deletion requested; waiting 30s")
134+
logger.debug(f"HelmRelease {self.hr_name} deletion requested; waiting 30s")
119135
time.sleep(30)
120136

121137
def _apply_helmrelease(self, api_client: ApiClient) -> None:
@@ -132,9 +148,9 @@ def _apply_helmrelease(self, api_client: ApiClient) -> None:
132148
path_params={
133149
'group': HR_GROUP,
134150
'version': HR_VERSION,
135-
'namespace': HR_NAMESPACE,
151+
'namespace': self.hr_namespace,
136152
'plural': HR_PLURAL,
137-
'name': HR_NAME,
153+
'name': self.hr_name,
138154
},
139155
query_params=[('fieldManager', 'plugin_t8s'), ('force', 'true')],
140156
header_params={
@@ -148,15 +164,15 @@ def _apply_helmrelease(self, api_client: ApiClient) -> None:
148164
auth_settings=['BearerToken'],
149165
_return_http_data_only=True,
150166
)
151-
logger.debug(f"HelmRelease {HR_NAME} applied")
167+
logger.debug(f"HelmRelease {self.hr_name} applied")
152168

153169
def _get_kubeconfig_from_secret(self, core_api: CoreV1Api) -> bytes:
154-
secret = cast(V1Secret, core_api.read_namespaced_secret(HR_KUBECONFIG_SECRET, HR_NAMESPACE))
170+
secret = cast(V1Secret, core_api.read_namespaced_secret(self.hr_kubeconfig_secret, self.hr_namespace))
155171
data: dict[str, str] = secret.data or {}
156172
if "value" in data:
157173
return base64.standard_b64decode(data["value"].encode())
158174
raise RuntimeError(
159-
f"kubeconfig secret {HR_KUBECONFIG_SECRET} is missing key 'value'; has keys: {list(data)}"
175+
f"kubeconfig secret {self.hr_kubeconfig_secret} is missing key 'value'; has keys: {list(data)}"
160176
)
161177

162178
def _wait_for_kubeconfig_secret(self, core_api: CoreV1Api) -> bytes:
@@ -170,10 +186,10 @@ def _wait_for_kubeconfig_secret(self, core_api: CoreV1Api) -> bytes:
170186
timeout = next(timeouts)
171187
if not timeout:
172188
raise RuntimeError(
173-
f"Timeout waiting for kubeconfig secret {HR_KUBECONFIG_SECRET}"
189+
f"Timeout waiting for kubeconfig secret {self.hr_kubeconfig_secret}"
174190
)
175191
logger.debug(
176-
f"waiting {timeout}s for kubeconfig secret {HR_KUBECONFIG_SECRET}"
192+
f"waiting {timeout}s for kubeconfig secret {self.hr_kubeconfig_secret}"
177193
)
178194
time.sleep(timeout)
179195

Tests/kaas/plugin/test_plugin_t8s.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,7 @@
88
from kubernetes.client import ApiException, V1Secret
99

1010
from plugin_t8s import (
11-
HR_CHART_REF,
1211
HR_GROUP,
13-
HR_NAME,
14-
HR_NAMESPACE,
15-
HR_KUBECONFIG_SECRET,
16-
HR_VALUES_FIXED,
1712
HR_VERSION,
1813
PluginT8s,
1914
)
@@ -62,13 +57,13 @@ def test_helmrelease_kind(plugin):
6257

6358
def test_helmrelease_name_namespace(plugin):
6459
hr = plugin._build_helmrelease()
65-
assert hr["metadata"]["name"] == HR_NAME
66-
assert hr["metadata"]["namespace"] == HR_NAMESPACE
60+
assert hr["metadata"]["name"] == plugin.hr_name
61+
assert hr["metadata"]["namespace"] == plugin.hr_namespace
6762

6863

6964
def test_helmrelease_chartref(plugin):
7065
hr = plugin._build_helmrelease()
71-
assert hr["spec"]["chartRef"] == HR_CHART_REF
66+
assert hr["spec"]["chartRef"] == plugin.hr_chart_ref
7267
assert hr["spec"]["chartRef"]["kind"] == "HelmChart"
7368
assert hr["spec"]["chartRef"]["name"] == "scs-kaas-certification"
7469
assert hr["spec"]["chartRef"]["namespace"] == "flux-system"
@@ -135,6 +130,59 @@ def test_helmrelease_single_nodepool(plugin):
135130
assert "pool-0" in nodepools
136131

137132

133+
def _make_plugin(tmp_path, **config_overrides):
134+
tpl = tmp_path / "t8s-kubeconfig.yaml"
135+
tpl.write_text(KUBECONFIG_TEMPLATE)
136+
config = {
137+
"kubernetesVersion": "1.35",
138+
"version_patch": 2,
139+
"templates": {"kubeconfig": tpl.name},
140+
"secrets": {},
141+
**config_overrides,
142+
}
143+
with patch("k8s_helper.setup_client_config"):
144+
return PluginT8s(config, basepath=str(tmp_path), cwd=str(tmp_path))
145+
146+
147+
def test_helmrelease_values_from_config(tmp_path):
148+
plugin = _make_plugin(
149+
tmp_path,
150+
cloud="other-cloud",
151+
controlPlaneHosted=False,
152+
nodePools={"pool-1": {"flavor": "custom.flavor", "replicas": 5}},
153+
metadata={
154+
"customerID": 2222,
155+
"customerName": "Other Customer GmbH",
156+
"friendlyName": "other-friendly-name",
157+
"serviceLevelAgreement": "Gold",
158+
},
159+
)
160+
values = plugin._build_helmrelease()["spec"]["values"]
161+
assert values["cloud"] == "other-cloud"
162+
assert values["controlPlane"]["hosted"] is False
163+
assert values["nodePools"] == {"pool-1": {"flavor": "custom.flavor", "replicas": 5}}
164+
assert values["metadata"] == {
165+
"customerID": 2222,
166+
"customerName": "Other Customer GmbH",
167+
"friendlyName": "other-friendly-name",
168+
"serviceLevelAgreement": "Gold",
169+
}
170+
171+
172+
def test_hr_name_defaults(plugin):
173+
assert plugin.hr_name == "scs-kaas-certification"
174+
assert plugin.hr_kubeconfig_secret == "scs-kaas-certification-kubeconfig"
175+
176+
177+
def test_hr_name_from_config(tmp_path):
178+
plugin = _make_plugin(tmp_path, name="other-target")
179+
assert plugin.hr_name == "other-target"
180+
assert plugin.hr_kubeconfig_secret == "other-target-kubeconfig"
181+
182+
hr = plugin._build_helmrelease()
183+
assert hr["metadata"]["name"] == "other-target"
184+
185+
138186
# --- version parsing ---
139187

140188
@pytest.mark.parametrize("version,version_patch,expected", [
@@ -178,7 +226,7 @@ def test_get_kubeconfig_from_secret(plugin):
178226
core_api = MagicMock()
179227
core_api.read_namespaced_secret.return_value = secret
180228
assert plugin._get_kubeconfig_from_secret(core_api) == raw
181-
core_api.read_namespaced_secret.assert_called_once_with(HR_KUBECONFIG_SECRET, HR_NAMESPACE)
229+
core_api.read_namespaced_secret.assert_called_once_with(plugin.hr_kubeconfig_secret, plugin.hr_namespace)
182230

183231

184232
def test_get_kubeconfig_from_secret_null_data(plugin):

0 commit comments

Comments
 (0)