|
| 1 | +import base64 |
| 2 | +import logging |
| 3 | +import os |
| 4 | +import os.path |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | +from interface import KubernetesClusterPlugin |
| 8 | +from jinja2 import Environment |
| 9 | +import kubernetes |
| 10 | +import yaml |
| 11 | + |
| 12 | +logger = logging.getLogger(__name__) |
| 13 | + |
| 14 | + |
| 15 | +TEMPLATE_KEYS = ('cluster', 'clusterstack', 'kubeconfig') |
| 16 | + |
| 17 | + |
| 18 | +def load_templates(env, basepath, fn_map, keys=TEMPLATE_KEYS): |
| 19 | + new_map = {} |
| 20 | + for key in keys: |
| 21 | + fn = fn_map.get(key) |
| 22 | + if fn is None: |
| 23 | + new_map[key] = None |
| 24 | + continue |
| 25 | + with open(os.path.join(basepath, fn), "r") as fileobj: |
| 26 | + new_map[key] = env.from_string(fileobj.read()) |
| 27 | + missing = [key for k, v in new_map.items() if v is None] |
| 28 | + if missing: |
| 29 | + raise RuntimeError(f'missing templates: {", ".join(missing)}') |
| 30 | + return new_map |
| 31 | + |
| 32 | + |
| 33 | +class PluginClusterStacks(KubernetesClusterPlugin): |
| 34 | + """ |
| 35 | + Plugin to handle the provisioning of kubernetes cluster for |
| 36 | + conformance testing purpose with the use of Kind |
| 37 | + """ |
| 38 | + def __init__(self, config, basepath='.', cwd='.'): |
| 39 | + self.basepath = basepath |
| 40 | + self.cwd = cwd |
| 41 | + self.config = config |
| 42 | + logger.debug(self.config) |
| 43 | + self.env = Environment() |
| 44 | + self.template_map = load_templates(self.env, self.basepath, self.config['templates']) |
| 45 | + self.vars = self.config['vars'] |
| 46 | + self.vars['name'] = self.config['name'] |
| 47 | + self.secrets = self.config['secrets'] |
| 48 | + self.kubeconfig = yaml.load( |
| 49 | + self.template_map['kubeconfig'].render(**self.vars, **self.secrets), |
| 50 | + Loader=yaml.SafeLoader, |
| 51 | + ) |
| 52 | + self.client_config = kubernetes.client.Configuration() |
| 53 | + token = self.kubeconfig['users'][0]['user']['token'] |
| 54 | + self.client_config.api_key['authorization'] = 'Bearer {}'.format(token) |
| 55 | + self.client_config.host = self.kubeconfig['clusters'][0]['cluster']['server'] |
| 56 | + self.client_config.ssl_ca_cert = os.path.abspath(os.path.join(self.cwd, 'ca.crt')) |
| 57 | + with open(self.client_config.ssl_ca_cert, "wb") as fileobj: |
| 58 | + fileobj.write(base64.standard_b64decode(self.kubeconfig['clusters'][0]['cluster']['certificate-authority-data'].encode())) |
| 59 | + self.namespace = self.kubeconfig['contexts'][0]['context']['namespace'] |
| 60 | + |
| 61 | + def auto_vars_syself(self, api_client): |
| 62 | + # beware: the following is quite the incantation |
| 63 | + prefix = f"v{self.config['kubernetesVersion']}" |
| 64 | + api_instance = kubernetes.client.CustomObjectsApi(api_client) |
| 65 | + # mimic `kubectl get clusterstackrelease` (it's a bit more involved with the API) |
| 66 | + res = api_instance.list_namespaced_custom_object('clusterstack.x-k8s.io', 'v1alpha1', self.namespace, 'clusterstackreleases') |
| 67 | + # filter items by readiness and kubernetesVersion, select fields of interest: name, version |
| 68 | + items = [ |
| 69 | + (item['metadata']['name'], item['status']['kubernetesVersion']) |
| 70 | + for item in res['items'] |
| 71 | + if item['status']['ready'] |
| 72 | + if item['status']['kubernetesVersion'].startswith(prefix) |
| 73 | + ] |
| 74 | + # sort filtered result by patch version |
| 75 | + items.sort(key=lambda item: item[1].rsplit('.', 1)[-1]) |
| 76 | + # select latest |
| 77 | + cs_class_name, cs_version = items[-1] |
| 78 | + self.vars.setdefault('cs_class_name', cs_class_name) |
| 79 | + self.vars.setdefault('cs_version', cs_version) |
| 80 | + |
| 81 | + def create_cluster(self): |
| 82 | + with kubernetes.client.ApiClient(self.client_config) as api_client: |
| 83 | + if self.config.get('autoVars') == 'syself': |
| 84 | + self.auto_vars_syself(api_client) |
| 85 | + # write out cluster.yaml for purposes of documentation; we won't use kubectl apply -f |
| 86 | + cluster_yaml = self.template_map['cluster'].render(**self.vars) |
| 87 | + cluster_dict = yaml.load(cluster_yaml, Loader=yaml.SafeLoader) |
| 88 | + with open(os.path.join(self.cwd, 'cluster.yaml'), "w") as fileobj: |
| 89 | + fileobj.write(cluster_yaml) |
| 90 | + # kubernetes.utils.create_from_dict(api_client, cluster_dict) |
| 91 | + api_instance = kubernetes.client.CustomObjectsApi(api_client) |
| 92 | + # mimic `kubectl apply -f` (it's a bit more involved with the API) |
| 93 | + res = api_instance.create_namespaced_custom_object('cluster.x-k8s.io', 'v1beta1', self.namespace, 'clusters', cluster_dict, field_manager='plugin_clusterstacks') |
| 94 | + |
| 95 | + def delete_cluster(self): |
| 96 | + cluster_name = self.config['name'] |
| 97 | + KindCluster(cluster_name).delete() |
0 commit comments