Skip to content

Commit a390a6a

Browse files
committed
Add Gardener plugin
based on d01dc2b (initial working state vibe-coded by @benedikt-haug) Signed-off-by: Matthias Büchse <matthias.buechse@alasca.cloud>
1 parent 4dc76d9 commit a390a6a

8 files changed

Lines changed: 447 additions & 0 deletions

File tree

.zuul.d/secure.yaml

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,96 @@
364364
uUgvW2Lghmg0+upQ1OfAV24lRryyUcpSYZ6yfohTgJsIMdy7r/Hn5GDnpZn2Um8rHkWxC
365365
rk7h2aWsD5IifYyt43ZHZdEdBX99NW+TMawXNZRevlmjoqgR4RGjo0Hxi4zSaqkNPHm/T
366366
YJcPPaJvKONZzrL3d+AWnhp6x/wi2uOBTFkrg8VmnZDiFf5YCZu/XMHYT5cjd0=
367+
noris_api_server: !encrypted/pkcs1-oaep
368+
- s9igorN8BJsXlQiQ0UUl038/8kw2qNO5Uf1vVTctedulJfWy+vSYBe8L7r+AVfaJ5c1vL
369+
2Fa91q00QQL0k6BV08WJYiK5leSWcbP7mYUveysUX4itDLo9xIYMCicNKW2UugNFzG2pE
370+
+h7zmoTuZlnE/25ajGQt83XLn3G24pUW+S5mWat7wC37rZOGpJ4SVS/XiIkFDJwzrvR3Y
371+
onZ3XdewFHybT6fa3UdVLhd1Wg2SFmyAJLkLCkVtr7c9GdUyl6ZBIYMqAlCKHJODp3Irg
372+
pwPGJo3RZJ9y+aKrRmrNM8S/clzBh3v/ANIyOSMMGQ0cZqmHi+2DstE8U+FQzrJWZVQCn
373+
mjU5S4TqIJfjopvliJjHsvjk5vRHCVPTw+kCRO2RYy8WS7mzaY7KQxF0a978KGewGVCE4
374+
mwaIz97YwCRl+YBPpjLArrhquG/lScs+vtevccaigJdlHp2tFiTfvNg66dsCmr3o1DJGY
375+
2bBWW6xV6lh+gtSEPXk6xqinsKkK/b822minK+bFUgtjrhB/GyuPgbpl5SRfq1n/RoTR4
376+
KwCKxn+wHBuQ2Fm/v6ECroUjV6XwIh5vPMsFCzy2zLL20xnZx9KRJpmsi6JEVhkT9zTJZ
377+
AoQagaWgkhNywrFo1orKqRefZ+hrqHXTRbSZ+StGjpmvjxkDkU883+1qfIqYzE=
378+
noris_ca_data: !encrypted/pkcs1-oaep
379+
- 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
- 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
- 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
- Dw3GFRbVvRauxXqZyQbly9hzYZlSt64TwnnG3qgAU1EWW77msHWEFUNfYlFx4aJO4Chkt
410+
vhQ+fuYXX5ZowU4MFAtd3vPTXAtOA8dJ+lAzfcnN4sc0dg7BsKjjPRHbcA/CqCdb1cMfv
411+
vyJCiEYpf80Wn0hfh9VifSgEt5VI3UyXrwCzBWDXcTR6qzFdKvQ0KpLKnqNRgFtgPozZu
412+
1quy8gaDR+OY+lGrU3x0IBRyxpK9UX5MSFRpk+81JCAuPBWxRtUu5vOi+BE5auv9Yd7nq
413+
AsXi4G2FzwobQES0Y6fQgJlr1ii8WpS7m/9FsXubeTEKJi6aa+MkUYEWzObJYCbbQ75vg
414+
eyquTcLKiGuKPQM+Nxlo4BjD5iVlRTC+5DEk+5qaJwjqmnf04wdlch2UtnNne1I1jshxC
415+
H4FD4wfAIDBwkAMgol8UKO5sDp+uWn14hGF7KNoaRFfCaY89D0rOL/oxBxjzSq013hiBb
416+
yCrtFkFLkcvk75G0kbIeQcQQIOYOWaypNN6aS/6+eRbhG751XTGN11qkYwcnBMCvGyXc3
417+
sK4k3Fp7ewsIKsCsdJdwbsX8qpDLN6Mss0d4ftkkti20jw2ey4ymJrfPNcjq8SPay3YTG
418+
yOCRZJKGEhbq4jht34WpLUQz3bbKmeN1LRkST+vitAEI2rkbedIE7BtURbL444=
419+
- HdgKO7AY2aEWPIhizhSNKaTs1nnXwLCavzSJmBVk+nitwOdf2YD1SNfWOUGGIhzbIKAmy
420+
28u46hML/Z1zfylAWAEgig08c++43IV/JIgryGSJHNTjM40laA/KH4NvjZ7ZdZN5DURVN
421+
45iP01F5FwRtzy+aJtfrkIt8Rrpi3Bi+tiXIcGZrfJodhcmCbDjLkWfwPvyPVpAkr5rSP
422+
N2nijJL2jb5jPND0d3xHY3YQCSSSUDg4o9CYRoL60O432J5Jx42XAYQzHqdT+qr8vQe9P
423+
E2Ye+xYFzinP4gldpLg/8n4/CbfxND7bSkBopwBjZKdgACCRw7/ratFzXiUTQJsdBPJtR
424+
XA0f2O+InI0WCU0dIyQCollczRcIR15YQXVnrD5XrIjYBfQTJTwtD5eCKgO1/DyHTfbK3
425+
edZXuArEG7HrHzpo/di5t/0c3CNvOkpWjmoF4VW0jwIOhb0mOytBK+LEtalgiGIN+fLqC
426+
Iek4Bf2Vsh3IcXe4WPgooBm9y0wejEmRVwApwcsArxILaa/VUJQO6zVeaLKTGTBNVKolm
427+
pP2x748l+uFWMUGjmzao4OTyfjBKx2cXNqe/OmIyscCr7lDZkipFdDTWrR1p/FLHQiCIc
428+
jBsgwuDMuVcVslw+cy3CPiPKZNWSJLop5cIvCEzGPBzXto9YTVQwPwnmcl+Rnc=
429+
noris_token: !encrypted/pkcs1-oaep
430+
- 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
- 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
- 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

Tests/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ scopes = [
4040
"scs-compatible-kaas",
4141
]
4242
subjects = [
43+
"noris-1.34",
4344
"syself-1.33",
4445
"syself-1.34",
4546
]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""helper functions for Gardener plugin"""
2+
import base64
3+
import os
4+
5+
from kubernetes.client import Configuration, CoreV1Api, CustomObjectsApi
6+
7+
8+
GARDENER_GROUP = 'core.gardener.cloud'
9+
GARDENER_VERSION = 'v1beta1'
10+
GARDENER_PLURAL = 'shoots'
11+
12+
13+
def setup_client_config(client_config: Configuration, kubeconfig, cwd='.'):
14+
"""transfer authentication data from kubeconfig to client_config, creating file `ca.crt`s"""
15+
token = kubeconfig['users'][0]['user']['token']
16+
client_config.api_key['authorization'] = 'Bearer {}'.format(token)
17+
client_config.host = kubeconfig['clusters'][0]['cluster']['server']
18+
client_config.ssl_ca_cert = os.path.abspath(os.path.join(cwd, 'ca.crt'))
19+
with open(client_config.ssl_ca_cert, "wb") as fileobj:
20+
fileobj.write(base64.standard_b64decode(
21+
kubeconfig['clusters'][0]['cluster']['certificate-authority-data'].encode()
22+
))
23+
24+
25+
def create_shoot(co_api: CustomObjectsApi, namespace: str, body: dict):
26+
return co_api.create_namespaced_custom_object(
27+
group=GARDENER_GROUP,
28+
version=GARDENER_VERSION,
29+
namespace=namespace,
30+
plural=GARDENER_PLURAL,
31+
body=body,
32+
)
33+
34+
35+
def delete_shoot(co_api: CustomObjectsApi, namespace: str, name: str):
36+
return co_api.delete_namespaced_custom_object(
37+
group=GARDENER_GROUP, version=GARDENER_VERSION, namespace=namespace, plural=GARDENER_PLURAL, name=name,
38+
)
39+
40+
41+
def annotate_shoot_confirm_deletion(co_api: CustomObjectsApi, namespace: str, name: str):
42+
shoot_patch = {
43+
"metadata": {
44+
"annotations": {
45+
"confirmation.gardener.cloud/deletion": "true",
46+
}
47+
}
48+
}
49+
co_api.patch_namespaced_custom_object(
50+
group=GARDENER_GROUP,
51+
version=GARDENER_VERSION,
52+
namespace=namespace,
53+
plural=GARDENER_PLURAL,
54+
name=name,
55+
body=shoot_patch,
56+
)
57+
58+
59+
def get_shoot_status(co_api: CustomObjectsApi, namespace: str, name: str):
60+
return co_api.get_namespaced_custom_object_status(
61+
group=GARDENER_GROUP,
62+
version=GARDENER_VERSION,
63+
namespace=namespace,
64+
plural=GARDENER_PLURAL,
65+
name=name,
66+
)
67+
68+
# def get_secret_data(core_api: CoreV1Api, namespace: str, name: str):
69+
# secret = core_api.read_namespaced_secret(name, namespace)
70+
# return base64.b64decode(secret.data['kubeconfig'])
71+
def get_secret_data(api_instance: CoreV1Api, namespace, secret):
72+
"""mimic `kubectl get secrets NAME -o=jsonpath='{.data.value}' | base64 -d > kubeconfig.yaml`"""
73+
res = api_instance.read_namespaced_secret(secret, namespace)
74+
return base64.standard_b64decode(res.data['value'].encode())
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import logging
2+
import os
3+
import os.path
4+
import time
5+
6+
from jinja2 import Environment, StrictUndefined
7+
from kubernetes.client import ApiClient, ApiException
8+
import yaml
9+
10+
from interface import KubernetesClusterPlugin
11+
import gardener_helper as _gh
12+
13+
logger = logging.getLogger(__name__)
14+
logging.getLogger("kubernetes").setLevel(logging.INFO)
15+
16+
17+
TEMPLATE_KEYS = ('shoot', 'kubeconfig')
18+
19+
20+
class _ShootOps:
21+
def __init__(self, namespace: str, name: str):
22+
self.namespace = namespace
23+
self.name = name
24+
self.secret_name = f'{name}.kubeconfig'
25+
26+
def _get_last_operation(self, co_api: _gh.CustomObjectsApi):
27+
try:
28+
status = _gh.get_shoot_status(co_api, self.namespace, self.name).get('status', {})
29+
return status.get('lastOperation', {})
30+
except ApiException as e:
31+
if e.status != 404:
32+
raise
33+
return None
34+
35+
def create(self, co_api: _gh.CustomObjectsApi, shoot_dict):
36+
# Gardener shoot reconciliation is idempotent, so we can just try to create.
37+
# If it exists, we check its state.
38+
# repeat this because it's possible that a cluster object exists that is in state Delete
39+
while True:
40+
logger.debug(f"creating shoot object for {self.name}")
41+
try:
42+
_gh.create_shoot(co_api, self.namespace, shoot_dict)
43+
except ApiException as e:
44+
# 409 means that the object already exists
45+
if e.status != 409:
46+
raise
47+
# check state: if it's Processing or Succeeded, we are good
48+
last_op = self._get_last_operation(co_api)
49+
if last_op is None:
50+
logger.debug(f'cluster object for {self.name} was present, has disappeared, retry')
51+
continue
52+
state = last_op.get('state', 'Unknown')
53+
logger.debug(f"Shoot object for {self.name} already present in state {state}")
54+
if state in ('Succeeded', 'Processing'):
55+
break
56+
if state == 'Failed':
57+
raise RuntimeError(f"Shoot {self.name} is in Failed state. Please check the Gardener dashboard.")
58+
59+
logger.debug(f'waiting 30 s for shoot {self.name} to proceed')
60+
time.sleep(30)
61+
else:
62+
break
63+
64+
def delete(self, co_api: _gh.CustomObjectsApi):
65+
# Add deletion confirmation annotation to the shoot before deleting.
66+
# This is required by Gardener.
67+
try:
68+
_gh.annotate_shoot_confirm_deletion(co_api, self.namespace, self.name)
69+
_gh.delete_shoot(co_api, self.namespace, self.name)
70+
except ApiException as e:
71+
if e.status == 404:
72+
logger.debug(f"Shoot {self.name} not present")
73+
return
74+
raise
75+
76+
logger.debug(f"Shoot {self.name} deletion requested; waiting 8 s for it to start deleting")
77+
time.sleep(8)
78+
while True:
79+
last_op = self._get_last_operation(co_api)
80+
if last_op is None:
81+
logger.info(f"Shoot {self.name} has been deleted.")
82+
break
83+
84+
if last_op.get('type') != 'Delete':
85+
raise RuntimeError(f"Shoot {self.name} in unexpected state during deletion: {last_op!r}")
86+
state = last_op.get('state', 'Unknown')
87+
if state not in ('Processing', 'Succeeded'):
88+
raise RuntimeError(f"Shoot {self.name} in unexpected state during deletion: state={state}")
89+
90+
if state == 'Processing':
91+
progress = last_op.get('progress', 0)
92+
logger.debug(f"Shoot {self.name} is being deleted (progress: {progress} %); waiting 30 s")
93+
elif state == 'Succeeded':
94+
logger.info(f"Shoot {self.name} deletion succeeded, but object still exists. Waiting 30 s for it to vanish.")
95+
time.sleep(30)
96+
97+
def get_kubeconfig(self, core_api: _gh.CoreV1Api):
98+
return _gh.get_secret_data(core_api, self.namespace, self.secret_name)
99+
100+
def wait_for_shoot_ready(self, co_api: _gh.CustomObjectsApi):
101+
last_op = self._get_last_operation(co_api)
102+
while last_op is not None and last_op.get('state') != 'Succeeded':
103+
state = last_op.get('state', 'Unknown')
104+
progress = last_op.get('progress', 0)
105+
logger.debug(f'waiting 30 s for shoot {self.name} to become ready (current state: {state}, progress: {progress}%)')
106+
time.sleep(30)
107+
last_op = self._get_last_operation(co_api)
108+
if last_op is None:
109+
raise RuntimeError(f"Shoot {self.name} object disappeared")
110+
logger.debug(f'shoot {self.name} appears to be ready')
111+
112+
113+
def load_templates(env, basepath, fn_map, keys=TEMPLATE_KEYS):
114+
new_map = {}
115+
for key in keys:
116+
fn = fn_map.get(key)
117+
if fn is None:
118+
new_map[key] = None
119+
continue
120+
with open(os.path.join(basepath, fn), "r") as fileobj:
121+
new_map[key] = env.from_string(fileobj.read())
122+
missing = [key for k, v in new_map.items() if v is None]
123+
if missing:
124+
raise RuntimeError(f'missing templates: {", ".join(missing)}')
125+
return new_map
126+
127+
128+
class PluginGardener(KubernetesClusterPlugin):
129+
"""
130+
Plugin to handle the provisioning of Kubernetes clusters via Gardener
131+
to be used for conformance testing.
132+
"""
133+
def __init__(self, plugin_config, basepath='.', cwd='.', name=None):
134+
self.basepath = basepath
135+
self.cwd = cwd
136+
self.config = plugin_config
137+
self.env = Environment(undefined=StrictUndefined)
138+
self.template_map = load_templates(self.env, self.basepath, self.config['templates'])
139+
self.vars = self.config['vars']
140+
self.vars['name'] = self.config['name']
141+
self.secrets = self.config['secrets']
142+
self.kubeconfig = yaml.load(self._render_template('kubeconfig'), Loader=yaml.SafeLoader)
143+
self.client_config = _gh.Configuration()
144+
_gh.setup_client_config(self.client_config, self.kubeconfig, cwd=self.cwd)
145+
self.namespace = self.kubeconfig['contexts'][0]['context']['namespace']
146+
147+
def _render_template(self, key):
148+
return self.template_map[key].render(**self.vars, **self.secrets)
149+
150+
def _write_shoot_yaml(self, shoot_yaml):
151+
# write out shoot.yaml for purposes of documentation
152+
# we will however use the dict instead of calling the shell with `kubectl apply -f`
153+
shoot_yaml_path = os.path.join(self.cwd, 'shoot.yaml')
154+
logger.debug(f'writing out {shoot_yaml_path}')
155+
with open(shoot_yaml_path, "w") as fileobj:
156+
fileobj.write(shoot_yaml)
157+
158+
def _write_kubeconfig(self, kubeconfig):
159+
# write out kubeconfig.yaml
160+
kubeconfig_path = os.path.join(self.cwd, 'kubeconfig.yaml')
161+
logger.debug(f'writing out {kubeconfig_path}')
162+
with open(kubeconfig_path, 'wb') as fileobj:
163+
fileobj.write(kubeconfig)
164+
165+
def create_cluster(self):
166+
with ApiClient(self.client_config) as api_client:
167+
core_api = _gh.CoreV1Api(api_client)
168+
co_api = _gh.CustomObjectsApi(api_client)
169+
shoot_yaml = self._render_template('shoot')
170+
shoot_dict = yaml.load(shoot_yaml, Loader=yaml.SafeLoader)
171+
self._write_shoot_yaml(shoot_yaml)
172+
sops = _ShootOps(self.namespace, self.config['name'])
173+
sops.create(co_api=co_api, shoot_dict=shoot_dict)
174+
sops.wait_for_shoot_ready(co_api)
175+
self._write_kubeconfig(cops.get_kubeconfig(core_api))
176+
177+
def delete_cluster(self):
178+
with ApiClient(self.client_config) as api_client:
179+
co_api = _gh.CustomObjectsApi(api_client)
180+
_ShootOps(self.namespace, self.config['name']).delete(co_api)

Tests/kaas/plugin/run_plugin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
import yaml
77

88
from plugin_clusterstacks import PluginClusterStacks
9+
from plugin_gardener import PluginGardener
910
from plugin_kind import PluginKind
1011
from plugin_static import PluginStatic
1112

1213

1314
PLUGIN_LOOKUP = {
1415
"clusterstacks": PluginClusterStacks,
16+
"gardener": PluginGardener,
1517
"kind": PluginKind,
1618
"static": PluginStatic,
1719
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: v1
2+
kind: Config
3+
current-context: default
4+
contexts:
5+
- name: default
6+
context:
7+
cluster: garden
8+
user: tooluser
9+
namespace: "{{ garden_namespace }}"
10+
clusters:
11+
- name: garden
12+
cluster:
13+
server: "{{ garden_api_server }}"
14+
certificate-authority-data: "{{ garden_ca_data }}"
15+
users:
16+
- name: tooluser
17+
user:
18+
token: "{{ garden_token }}"

0 commit comments

Comments
 (0)