From 1149a5ca7c4b439520d2f121d6144f9e005f709e Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Thu, 22 Feb 2024 13:51:08 +0100 Subject: [PATCH 01/10] Improve code --- IM/connectors/Kubernetes.py | 35 ++++++++++++------------------ test/unit/connectors/Kubernetes.py | 4 +++- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/IM/connectors/Kubernetes.py b/IM/connectors/Kubernetes.py index 542db4c6..510d5f19 100644 --- a/IM/connectors/Kubernetes.py +++ b/IM/connectors/Kubernetes.py @@ -169,8 +169,7 @@ def _create_volumes(self, namespace, system, pod_name, auth_data, persistent=Fal name = "%s-%d" % (pod_name, cont) if persistent: - claim_data = {'apiVersion': 'v1', 'kind': 'PersistentVolumeClaim'} - claim_data['metadata'] = {'name': name, 'namespace': namespace} + claim_data = self._gen_basic_k8s_elem(namespace, name, 'PersistentVolumeClaim') claim_data['spec'] = {'accessModes': ['ReadWriteOnce'], 'resources': { 'requests': {'storage': disk_size}}} @@ -218,12 +217,7 @@ def create_service_data(self, namespace, name, outports, public, auth_data, vm): self.log_exception("Error creating service.") def _generate_service_data(self, namespace, name, outports, public): - service_data = {'apiVersion': 'v1', 'kind': 'Service'} - service_data['metadata'] = { - 'name': name, - 'namespace': namespace, - 'labels': {'name': name} - } + service_data = self._gen_basic_k8s_elem(namespace, name, 'Service') ports = [] if outports: @@ -269,12 +263,7 @@ def create_ingress(self, namespace, name, dns, port, auth_data): return False def _generate_ingress_data(self, namespace, name, dns, port): - ingress_data = {'apiVersion': 'networking.k8s.io/v1', 'kind': 'Ingress'} - ingress_data['metadata'] = { - 'name': name, - 'namespace': namespace, - 'labels': {'name': name} - } + ingress_data = self._gen_basic_k8s_elem(namespace, name, 'Ingress', 'networking.k8s.io/v1') host = None path = "/" @@ -326,6 +315,16 @@ def _generate_ingress_data(self, namespace, name, dns, port): return ingress_data + @staticmethod + def _gen_basic_k8s_elem(namespace, name, kind, version="v1"): + k8s_elem = {'apiVersion': version, 'kind': kind} + k8s_elem['metadata'] = { + 'name': name, + 'namespace': namespace, + 'labels': {'name': name} + } + return k8s_elem + @staticmethod def _get_env_variables(radl_system): env_vars = [] @@ -353,13 +352,7 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, tags): ports.append({'containerPort': outport.get_local_port(), 'protocol': outport.get_protocol().upper()}) - pod_data = {'apiVersion': 'v1', 'kind': 'Pod'} - pod_data['metadata'] = { - 'name': name, - 'namespace': namespace, - 'labels': {'name': name} - } - + pod_data = self._gen_basic_k8s_elem(namespace, name, 'Pod') # Add instance tags if tags: for k, v in tags.items(): diff --git a/test/unit/connectors/Kubernetes.py b/test/unit/connectors/Kubernetes.py index 6d81b25e..ea66c3ae 100755 --- a/test/unit/connectors/Kubernetes.py +++ b/test/unit/connectors/Kubernetes.py @@ -162,7 +162,9 @@ def test_20_launch(self, save_data, requests): exp_pvc = { "apiVersion": "v1", "kind": "PersistentVolumeClaim", - "metadata": {"name": "test-1", "namespace": "somenamespace"}, + "metadata": {"name": "test-1", + "namespace": "somenamespace", + 'labels': {'name': 'test-1'}}, "spec": { "accessModes": ["ReadWriteOnce"], "resources": {"requests": {"storage": 10737418240}}, From b20c9ac2ccb055254df08e4da716e51f23635118 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Thu, 22 Feb 2024 13:53:05 +0100 Subject: [PATCH 02/10] Improve code --- test/unit/connectors/Kubernetes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unit/connectors/Kubernetes.py b/test/unit/connectors/Kubernetes.py index ea66c3ae..1c7f8c6b 100755 --- a/test/unit/connectors/Kubernetes.py +++ b/test/unit/connectors/Kubernetes.py @@ -159,6 +159,9 @@ def test_20_launch(self, save_data, requests): success, _ = res[0] self.assertTrue(success, msg="ERROR: launching a VM.") + self.assertEqual(requests.call_args_list[0][0][1], + 'http://server.com:8080/api/v1/namespaces/somenamespace') + exp_pvc = { "apiVersion": "v1", "kind": "PersistentVolumeClaim", From 26e182ddbc245c2279e24f2594a4ecbb5bcd4a60 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 12:39:06 +0100 Subject: [PATCH 03/10] Improve k8s with configmaps --- IM/connectors/Kubernetes.py | 151 ++++++++++++++++++++++------- IM/tosca/Tosca.py | 8 +- test/unit/connectors/Kubernetes.py | 54 ++++++++--- 3 files changed, 162 insertions(+), 51 deletions(-) diff --git a/IM/connectors/Kubernetes.py b/IM/connectors/Kubernetes.py index 510d5f19..4fae9a23 100644 --- a/IM/connectors/Kubernetes.py +++ b/IM/connectors/Kubernetes.py @@ -18,6 +18,7 @@ import json import requests import time +import os import re from netaddr import IPNetwork, IPAddress try: @@ -155,36 +156,100 @@ def _create_volume_claim(self, claim_data, auth_data): self.log_exception("Error connecting with Kubernetes API server") return False + def _delete_config_map(self, namespace, cm_name, auth_data): + try: + self.log_debug("Deleting CM: %s/%s" % (namespace, cm_name)) + uri = "/api/v1/namespaces/%s/%s/%s" % (namespace, "configmaps", cm_name) + resp = self.create_request('DELETE', uri, auth_data) + + if resp.status_code == 404: + self.log_warn("Trying to remove a non existing ConfigMap: " + cm_name) + return True + elif resp.status_code != 200: + self.log_error("Error deleting the ConfigMap: " + resp.txt) + return False + else: + return True + except Exception: + self.log_exception("Error connecting with Kubernetes API server") + return False + + def _delete_config_maps(self, pod_data, auth_data): + if 'volumes' in pod_data['spec']: + for volume in pod_data['spec']['volumes']: + if 'configMap' in volume and 'name' in volume['configMap']: + cm_name = volume['configMap']['name'] + success = self._delete_config_map(pod_data["metadata"]["namespace"], cm_name, auth_data) + if not success: + self.log_error("Error deleting ConfigMap:" + cm_name) + + def _create_config_maps(self, namespace, system, pod_name, auth_data): + res = [] + cont = 1 + while system.getValue("disk." + str(cont) + ".mount_path"): + + if (system.getValue("disk." + str(cont) + ".content") and + not system.getValue("disk." + str(cont) + ".size")): + + mount_path = system.getValue("disk." + str(cont) + ".mount_path") + content = system.getValue("disk." + str(cont) + ".content") + if not mount_path.startswith('/'): + mount_path = '/' + mount_path + name = "%s-cm-%d" % (pod_name, cont) + + cm_data = self._gen_basic_k8s_elem(namespace, name, 'ConfigMap') + cm_data['data'] = {os.path.basename(mount_path): content} + + try: + self.log_debug("Creating ConfigMap: %s/%s" % (namespace, name)) + headers = {'Content-Type': 'application/json'} + uri = "/api/v1/namespaces/%s/%s" % (namespace, "configmaps") + svc_resp = self.create_request('POST', uri, auth_data, headers, cm_data) + if svc_resp.status_code != 201: + self.error_messages += "Error creating configmap for pod %s: %s" % (name, svc_resp.text) + self.log_warn("Error creating configmap: %s" % svc_resp.text) + else: + res.append((name, mount_path)) + except Exception: + self.error_messages += "Error creating configmap to access pod %s" % name + self.log_exception("Error creating configmap.") + + cont += 1 + + return res + def _create_volumes(self, namespace, system, pod_name, auth_data, persistent=False): res = [] cont = 1 - while ((system.getValue("disk." + str(cont) + ".size") or - system.getValue("disk." + str(cont) + ".image.url")) and - system.getValue("disk." + str(cont) + ".mount_path")): - volume_id = system.getValue("disk." + str(cont) + ".image.url") - disk_mount_path = system.getValue("disk." + str(cont) + ".mount_path") - disk_size = system.getFeature("disk." + str(cont) + ".size").getValue('B') - if not disk_mount_path.startswith('/'): - disk_mount_path = '/' + disk_mount_path - name = "%s-%d" % (pod_name, cont) - - if persistent: - claim_data = self._gen_basic_k8s_elem(namespace, name, 'PersistentVolumeClaim') - claim_data['spec'] = {'accessModes': ['ReadWriteOnce'], 'resources': { - 'requests': {'storage': disk_size}}} - - if volume_id: - claim_data['spec']['storageClassName'] = "" - claim_data['spec']['volumeName'] = volume_id - - self.log_debug("Creating PVC: %s/%s" % (namespace, name)) - success = self._create_volume_claim(claim_data, auth_data) - if success: - res.append((name, disk_size, disk_mount_path, persistent)) + while system.getValue("disk." + str(cont) + ".mount_path"): + + if (system.getValue("disk." + str(cont) + ".size") or + system.getValue("disk." + str(cont) + ".image.url")): + + volume_id = system.getValue("disk." + str(cont) + ".image.url") + disk_mount_path = system.getValue("disk." + str(cont) + ".mount_path") + disk_size = system.getFeature("disk." + str(cont) + ".size").getValue('B') + if not disk_mount_path.startswith('/'): + disk_mount_path = '/' + disk_mount_path + name = "%s-%d" % (pod_name, cont) + + if persistent: + claim_data = self._gen_basic_k8s_elem(namespace, name, 'PersistentVolumeClaim') + claim_data['spec'] = {'accessModes': ['ReadWriteOnce'], 'resources': { + 'requests': {'storage': disk_size}}} + + if volume_id: + claim_data['spec']['storageClassName'] = "" + claim_data['spec']['volumeName'] = volume_id + + self.log_debug("Creating PVC: %s/%s" % (namespace, name)) + success = self._create_volume_claim(claim_data, auth_data) + if success: + res.append((name, disk_size, disk_mount_path, persistent)) + else: + self.log_error("Error creating PersistentVolumeClaim:" + name) else: - self.log_error("Error creating PersistentVolumeClaim:" + name) - else: - res.append((name, disk_size, disk_mount_path, persistent)) + res.append((name, disk_size, disk_mount_path, persistent)) cont += 1 @@ -337,7 +402,7 @@ def _get_env_variables(radl_system): env_vars.append({'name': key, 'value': value}) return env_vars - def _generate_pod_data(self, namespace, name, outports, system, volumes, tags): + def _generate_pod_data(self, namespace, name, outports, system, volumes, configmaps, tags): cpu = str(system.getValue('cpu.count')) memory = "%s" % system.getFeature('memory.size').getValue('B') image_url = urlparse(system.getValue("disk.0.image.url")) @@ -381,6 +446,12 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, tags): containers[0]['volumeMounts'].append( {'name': v_name, 'mountPath': v_mount_path}) + if configmaps: + containers[0]['volumeMounts'] = containers[0].get('volumeMounts', []) + for (cm_name, cm_mount_path) in configmaps: + containers[0]['volumeMounts'].append( + {'name': cm_name, 'mountPath': cm_mount_path, 'readOnly': True}) + pod_data['spec'] = {'containers': containers, 'restartPolicy': 'OnFailure'} if volumes: @@ -390,6 +461,13 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, tags): pod_data['spec']['volumes'].append( {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) + if configmaps: + for (cm_name, _) in configmaps: + pod_data['spec']['volumes'].append( + {'name': cm_name, + 'configMap': {'name': cm_name, + 'items': [{'key': os.path.basename(cm_mount_path), 'path': cm_mount_path}]}}) + return pod_data @staticmethod @@ -439,6 +517,8 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): volumes = self._create_volumes(namespace, system, pod_name, auth_data, True) + configmaps = self._create_config_maps(namespace, system, pod_name, auth_data) + tags = self.get_instance_tags(system, auth_data, inf) outports = [] @@ -449,7 +529,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): elif priv_net: outports = priv_net.getOutPorts() - pod_data = self._generate_pod_data(namespace, pod_name, outports, system, volumes, tags) + pod_data = self._generate_pod_data(namespace, pod_name, outports, system, volumes, configmaps, tags) self.log_debug("Creating POD: %s/%s" % (namespace, pod_name)) uri = "/api/v1/namespaces/%s/%s" % (namespace, "pods") @@ -462,15 +542,20 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): self._delete_volume_claims(pod_data, auth_data) except Exception: self.log_exception("Error deleting volumes.") - else: - dns_name = system.getValue("net_interface.0.dns_name") - - self.create_service_data(namespace, pod_name, outports, pub_net, auth_data, vm) + try: + self._delete_config_maps(pod_data, auth_data) + except Exception: + self.log_exception("Error deleting configmaps.") + else: output = json.loads(resp.text) vm.id = output["metadata"]["name"] vm.info.systems[0].setValue('instance_id', str(vm.id)) vm.info.systems[0].setValue('instance_name', str(vm.id)) + vm.destroy = False + + dns_name = system.getValue("net_interface.0.dns_name") + self.create_service_data(namespace, pod_name, outports, pub_net, auth_data, vm) if dns_name and outports: port = outports[0].get_local_port() @@ -478,7 +563,6 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): if not ingress_created: vm.info.systems[0].delValue("net_interface.0.dns_name") - vm.destroy = False res.append((True, vm)) except Exception as ex: @@ -568,6 +652,7 @@ def finalize(self, vm, last, auth_data): else: pod_data = json.loads(output) self._delete_volume_claims(pod_data, auth_data) + self._delete_config_maps(pod_data, auth_data) success, msg = self._delete_pod(vm, auth_data) if not success: self.log_error("Error deleting Pod %s: %s" % (vm.id, msg)) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index d89946a1..283a5d2e 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -2222,12 +2222,10 @@ def _gen_k8s_system(self, node, nodetemplates): res = system(node.name) nets = [] - artifacts = node.type_definition.get_value('artifacts', node.entity_tpl, True) - if len(artifacts) != 1: - raise Exception("Only one artifact is supported for K8s container.") + for artifact in list(self._get_node_artifacts(node).values()): + if artifact.get("type", None) == "tosca.artifacts.Deployment.Image.Container.Docker": + image = self._final_function_result(artifact.get("file", None), node) - artifact = list(artifacts.values())[0] - image = self._final_function_result(artifact.get("file", None), node) if not image: raise Exception("No image specified for K8s container.") if "tosca.artifacts.Deployment.Image.Container.Docker" != artifact.get("type", None): diff --git a/test/unit/connectors/Kubernetes.py b/test/unit/connectors/Kubernetes.py index 1c7f8c6b..3b06c41f 100755 --- a/test/unit/connectors/Kubernetes.py +++ b/test/unit/connectors/Kubernetes.py @@ -85,7 +85,8 @@ def get_response(self, method, url, verify, headers, data): resp.text = ('{"metadata": {"namespace":"somenamespace", "name": "name"}, "status": ' '{"phase":"Running", "hostIP": "158.42.1.1", "podIP": "10.0.0.1"}, ' '"spec": {"containers": [{"image": "image:1.0"}], ' - '"volumes": [{"persistentVolumeClaim": {"claimName" : "cname"}}]}}') + '"volumes": [{"persistentVolumeClaim": {"claimName" : "cname"}},' + '{"configMap": {"name": "configmap"}}]}}') if url == "/api/v1/namespaces/somenamespace": resp.status_code = 200 elif method == "POST": @@ -100,6 +101,8 @@ def get_response(self, method, url, verify, headers, data): resp.status_code = 201 elif url.endswith("/apis/networking.k8s.io/v1/namespaces/somenamespace/ingresses"): resp.status_code = 201 + elif url.endswith("/configmaps"): + resp.status_code = 201 elif method == "DELETE": if url.endswith("/pods/1"): resp.status_code = 200 @@ -111,6 +114,8 @@ def get_response(self, method, url, verify, headers, data): resp.status_code = 200 elif "ingresses" in url: resp.status_code = 200 + elif "configmaps" in url: + resp.status_code = 200 elif method == "PATCH": if url.endswith("/pods/1"): resp.status_code = 200 @@ -139,7 +144,11 @@ def test_20_launch(self, save_data, requests): disk.0.os.name = 'linux' and disk.0.image.url = 'docker://someimage' and disk.1.size = 10G and - disk.1.mount_path = '/mnt' + disk.1.mount_path = '/mnt' and + disk.2.mount_path = '/etc/config' and + disk.2.content = ' + some content + ' )""" radl = radl_parse.parse_radl(radl_data) radl.check() @@ -177,6 +186,18 @@ def test_20_launch(self, save_data, requests): 'http://server.com:8080/api/v1/namespaces/somenamespace/persistentvolumeclaims') self.assertEqual(json.loads(requests.call_args_list[1][1]['data']), exp_pvc) + exp_cm = { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": {"name": "test-cm-2", + "namespace": "somenamespace", + 'labels': {'name': 'test-cm-2'}}, + "data": {"config": "\n some content\n "}, + } + self.assertEqual(requests.call_args_list[2][0][1], + 'http://server.com:8080/api/v1/namespaces/somenamespace/configmaps') + self.assertEqual(json.loads(requests.call_args_list[2][1]['data']), exp_cm) + exp_pod = { "apiVersion": "v1", "kind": "Pod", @@ -197,18 +218,22 @@ def test_20_launch(self, save_data, requests): "requests": {"cpu": "1", "memory": "536870912"}, }, "env": [{"name": "var", "value": "some_val"}], - "volumeMounts": [{"name": "test-1", "mountPath": "/mnt"}], + "volumeMounts": [{"name": "test-1", "mountPath": "/mnt"}, + {'mountPath': '/etc/config', 'name': 'test-cm-2', 'readOnly': True}], } ], "restartPolicy": "OnFailure", "volumes": [ - {"name": "test-1", "persistentVolumeClaim": {"claimName": "test-1"}} + {"name": "test-1", "persistentVolumeClaim": {"claimName": "test-1"}}, + {"name": "test-cm-2", "configMap": {"name": "test-cm-2", + "items": [{"key": "config", "path": "/etc/config"}]}}, ], }, } - self.assertEqual(requests.call_args_list[2][0][1], + self.maxDiff = None + self.assertEqual(requests.call_args_list[3][0][1], 'http://server.com:8080/api/v1/namespaces/somenamespace/pods') - self.assertEqual(json.loads(requests.call_args_list[2][1]['data']), exp_pod) + self.assertEqual(json.loads(requests.call_args_list[3][1]['data']), exp_pod) exp_svc = { "apiVersion": "v1", @@ -232,9 +257,9 @@ def test_20_launch(self, save_data, requests): "selector": {"name": "test"}, }, } - self.assertEqual(requests.call_args_list[3][0][1], + self.assertEqual(requests.call_args_list[4][0][1], 'http://server.com:8080/api/v1/namespaces/somenamespace/services') - self.assertEqual(json.loads(requests.call_args_list[3][1]['data']), exp_svc) + self.assertEqual(json.loads(requests.call_args_list[4][1]['data']), exp_svc) self.maxDiff = None exp_ing = { @@ -275,9 +300,9 @@ def test_20_launch(self, save_data, requests): }, } - self.assertEqual(requests.call_args_list[5][0][1], + self.assertEqual(requests.call_args_list[6][0][1], 'http://server.com:8080/apis/networking.k8s.io/v1/namespaces/somenamespace/ingresses') - self.assertEqual(json.loads(requests.call_args_list[5][1]['data']), exp_ing) + self.assertEqual(json.loads(requests.call_args_list[6][1]['data']), exp_ing) self.assertNotIn("ERROR", self.log.getvalue(), msg="ERROR found in log: %s" % self.log.getvalue()) @@ -367,14 +392,17 @@ def test_60_finalize(self, requests): 'http://server.com:8080/api/v1/namespaces/somenamespace/persistentvolumeclaims/cname')) self.assertEqual(requests.call_args_list[2][0], ('DELETE', - 'http://server.com:8080/api/v1/namespaces/somenamespace/pods/1')) + 'http://server.com:8080/api/v1/namespaces/somenamespace/configmaps/configmap')) self.assertEqual(requests.call_args_list[3][0], ('DELETE', - 'http://server.com:8080/api/v1/namespaces/somenamespace/services/1')) + 'http://server.com:8080/api/v1/namespaces/somenamespace/pods/1')) self.assertEqual(requests.call_args_list[4][0], ('DELETE', - 'http://server.com:8080/apis/networking.k8s.io/v1/namespaces/somenamespace/ingresses/1')) + 'http://server.com:8080/api/v1/namespaces/somenamespace/services/1')) self.assertEqual(requests.call_args_list[5][0], + ('DELETE', + 'http://server.com:8080/apis/networking.k8s.io/v1/namespaces/somenamespace/ingresses/1')) + self.assertEqual(requests.call_args_list[6][0], ('DELETE', 'http://server.com:8080/api/v1/namespaces/somenamespace')) self.assertTrue(success, msg="ERROR: finalizing VM info.") From 5ed2eea2214ffa4162a27639e3f9b8cda2551119 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 12:40:31 +0100 Subject: [PATCH 04/10] Improve k8s with configmaps --- IM/tosca/Tosca.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index 283a5d2e..e1ba9e74 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -2228,8 +2228,7 @@ def _gen_k8s_system(self, node, nodetemplates): if not image: raise Exception("No image specified for K8s container.") - if "tosca.artifacts.Deployment.Image.Container.Docker" != artifact.get("type", None): - raise Exception("Only Docker images are supported for K8s container.") + repo = artifact.get("repository", None) if repo: repo_url = self._get_repository_url(repo) From 42e2eb2720d8ce5fc8ca60f7958b3e6c18c146b7 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 13:01:23 +0100 Subject: [PATCH 05/10] Improve k8s with configmaps --- IM/tosca/Tosca.py | 40 +++++++++++++++++++++++++++++++++------- test/files/tosca_k8s.yml | 10 ++++++++++ test/unit/Tosca.py | 2 ++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/IM/tosca/Tosca.py b/IM/tosca/Tosca.py index e1ba9e74..b5adbc26 100644 --- a/IM/tosca/Tosca.py +++ b/IM/tosca/Tosca.py @@ -35,7 +35,7 @@ class Tosca: """ ARTIFACTS_PATH = os.path.dirname(os.path.realpath(__file__)) + "/tosca-types/artifacts" - ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/" + ARTIFACTS_REMOTE_REPO = "https://raw.githubusercontent.com/grycap/tosca/main/artifacts/" GET_TIMEOUT = 20 logger = logging.getLogger('InfrastructureManager') @@ -2189,9 +2189,9 @@ def _get_oscar_service_json(self, node): return res - def _gen_k8s_volumes(self, node, nodetemplates, value): + def _gen_k8s_volumes(self, node, nodetemplates, value, cont=1): + """Get the volumes attached to an K8s container.""" volumes = [] - cont = 1 # volume format should be "volume_name:mount_path" for vol in value: size = None @@ -2217,18 +2217,44 @@ def _gen_k8s_volumes(self, node, nodetemplates, value): cont += 1 return volumes + def _gen_k8s_configmaps(self, res, cms): + """Get the configmaps attached to an K8s container.""" + cont = 1 + for cm in cms: + mount_path = cm.get("deploy_path") + cm_file = cm.get("file") + content = cm.get("properties", {}).get("content", "") + if content: + res.setValue('disk.%d.content' % cont, content) + # if content is not empty file is ignored + if cm_file and not content: + resp = self.cache_session.get(cm_file, timeout=self.GET_TIMEOUT) + if resp.status_code != 200: + raise Exception("Error downloading file %s: %s\n%s" % (cm_file, resp.reason, resp.text)) + res.setValue('disk.%d.content' % cont, resp.text) + if content or cm_file: + res.setValue('disk.%d.mount_path' % cont, mount_path) + cont += 1 + + return cont + def _gen_k8s_system(self, node, nodetemplates): - """Get the volumes attached to an K8s container.""" + """Generate the system for a K8s app.""" res = system(node.name) nets = [] + cms = [] for artifact in list(self._get_node_artifacts(node).values()): - if artifact.get("type", None) == "tosca.artifacts.Deployment.Image.Container.Docker": - image = self._final_function_result(artifact.get("file", None), node) + if artifact.get("type") == "tosca.artifacts.Deployment.Image.Container.Docker": + image = self._final_function_result(artifact.get("file"), node) + elif artifact.get("type") == "tosca.artifacts.File" and artifact.get("deploy_path"): + cms.append(artifact) if not image: raise Exception("No image specified for K8s container.") + cont = self._gen_k8s_configmaps(res, cms) + repo = artifact.get("repository", None) if repo: repo_url = self._get_repository_url(repo) @@ -2265,7 +2291,7 @@ def _gen_k8s_system(self, node, nodetemplates): value = int(ScalarUnit_Size(value).get_num_from_scalar_unit('B')) res.setValue("memory.size", value, 'B') elif prop.name == 'volumes': - for num, size, mount_path, volume_id in self._gen_k8s_volumes(node, nodetemplates, value): + for num, size, mount_path, volume_id in self._gen_k8s_volumes(node, nodetemplates, value, cont): if volume_id: res.setValue('disk.%d.image.url' % num, volume_id) if size: diff --git a/test/files/tosca_k8s.yml b/test/files/tosca_k8s.yml index 8b588040..eec042db 100644 --- a/test/files/tosca_k8s.yml +++ b/test/files/tosca_k8s.yml @@ -47,6 +47,16 @@ topology_template: my_image: file: grycap/im type: tosca.artifacts.Deployment.Image.Container.Docker + my_config_map: + deploy_path: /etc/im/im.cfg + file: https://raw.githubusercontent.com/grycap/im/master/etc/im.cfg + type: tosca.artifacts.File + properties: + # when the content is not provided, the file is downloaded from the URL + # otherwise, the file is ignored + content: | + [im] + REST_API = True # The properties of the runtime to host the container im_runtime: diff --git a/test/unit/Tosca.py b/test/unit/Tosca.py index 9248c9f3..9c383a36 100755 --- a/test/unit/Tosca.py +++ b/test/unit/Tosca.py @@ -455,6 +455,8 @@ def test_tosca_k8s(self): net = radl.get_network_by_id('im_container_pub') self.assertEqual(net.getValue("outports"), '30880/tcp-8800/tcp') self.assertEqual(net.getValue("outbound"), 'yes') + self.assertEqual(node.getValue("disk.1.content"), '[im]\nREST_API = True\n') + self.assertEqual(node.getValue("disk.1.mount_path"), '/etc/im/im.cfg') self.assertEqual(node.getValue("environment.variables"), 'IM_DATA_DB=mysql://root:my-secret@mysql-container:3306/im-db') self.assertEqual(node.getValue("net_interface.0.connection"), 'im_container_pub') From f1e14e3c2c4aaa57f78d3b9993cdd4e3b8c6a81f Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 13:28:23 +0100 Subject: [PATCH 06/10] Improve k8s with configmaps --- IM/connectors/Kubernetes.py | 7 ++++--- test/unit/connectors/Kubernetes.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/IM/connectors/Kubernetes.py b/IM/connectors/Kubernetes.py index 4fae9a23..b7d3ed08 100644 --- a/IM/connectors/Kubernetes.py +++ b/IM/connectors/Kubernetes.py @@ -450,7 +450,8 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, configm containers[0]['volumeMounts'] = containers[0].get('volumeMounts', []) for (cm_name, cm_mount_path) in configmaps: containers[0]['volumeMounts'].append( - {'name': cm_name, 'mountPath': cm_mount_path, 'readOnly': True}) + {'name': cm_name, 'mountPath': cm_mount_path, "readOnly": True, + 'subPath': os.path.basename(cm_mount_path)}) pod_data['spec'] = {'containers': containers, 'restartPolicy': 'OnFailure'} @@ -462,11 +463,11 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, configm {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) if configmaps: + pod_data['spec']['volumes'] = pod_data['spec'].get('volumes', []) for (cm_name, _) in configmaps: pod_data['spec']['volumes'].append( {'name': cm_name, - 'configMap': {'name': cm_name, - 'items': [{'key': os.path.basename(cm_mount_path), 'path': cm_mount_path}]}}) + 'configMap': {'name': cm_name}}) return pod_data diff --git a/test/unit/connectors/Kubernetes.py b/test/unit/connectors/Kubernetes.py index 3b06c41f..b1aade94 100755 --- a/test/unit/connectors/Kubernetes.py +++ b/test/unit/connectors/Kubernetes.py @@ -219,14 +219,14 @@ def test_20_launch(self, save_data, requests): }, "env": [{"name": "var", "value": "some_val"}], "volumeMounts": [{"name": "test-1", "mountPath": "/mnt"}, - {'mountPath': '/etc/config', 'name': 'test-cm-2', 'readOnly': True}], + {'mountPath': '/etc/config', 'name': 'test-cm-2', + 'readOnly': True, 'subPath': 'config'}], } ], "restartPolicy": "OnFailure", "volumes": [ {"name": "test-1", "persistentVolumeClaim": {"claimName": "test-1"}}, - {"name": "test-cm-2", "configMap": {"name": "test-cm-2", - "items": [{"key": "config", "path": "/etc/config"}]}}, + {"name": "test-cm-2", "configMap": {"name": "test-cm-2"}}, ], }, } From 2bcfa489378fc06afaf5d99c6995cf079febf9dc Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 13:41:59 +0100 Subject: [PATCH 07/10] Improve code --- IM/connectors/Kubernetes.py | 38 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/IM/connectors/Kubernetes.py b/IM/connectors/Kubernetes.py index b7d3ed08..14eb0650 100644 --- a/IM/connectors/Kubernetes.py +++ b/IM/connectors/Kubernetes.py @@ -218,7 +218,7 @@ def _create_config_maps(self, namespace, system, pod_name, auth_data): return res - def _create_volumes(self, namespace, system, pod_name, auth_data, persistent=False): + def _create_volumes(self, namespace, system, pod_name, auth_data): res = [] cont = 1 while system.getValue("disk." + str(cont) + ".mount_path"): @@ -233,23 +233,20 @@ def _create_volumes(self, namespace, system, pod_name, auth_data, persistent=Fal disk_mount_path = '/' + disk_mount_path name = "%s-%d" % (pod_name, cont) - if persistent: - claim_data = self._gen_basic_k8s_elem(namespace, name, 'PersistentVolumeClaim') - claim_data['spec'] = {'accessModes': ['ReadWriteOnce'], 'resources': { - 'requests': {'storage': disk_size}}} + claim_data = self._gen_basic_k8s_elem(namespace, name, 'PersistentVolumeClaim') + claim_data['spec'] = {'accessModes': ['ReadWriteOnce'], 'resources': { + 'requests': {'storage': disk_size}}} - if volume_id: - claim_data['spec']['storageClassName'] = "" - claim_data['spec']['volumeName'] = volume_id + if volume_id: + claim_data['spec']['storageClassName'] = "" + claim_data['spec']['volumeName'] = volume_id - self.log_debug("Creating PVC: %s/%s" % (namespace, name)) - success = self._create_volume_claim(claim_data, auth_data) - if success: - res.append((name, disk_size, disk_mount_path, persistent)) - else: - self.log_error("Error creating PersistentVolumeClaim:" + name) + self.log_debug("Creating PVC: %s/%s" % (namespace, name)) + success = self._create_volume_claim(claim_data, auth_data) + if success: + res.append((name, disk_size, disk_mount_path)) else: - res.append((name, disk_size, disk_mount_path, persistent)) + self.log_error("Error creating PersistentVolumeClaim:" + name) cont += 1 @@ -442,7 +439,7 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, configm if volumes: containers[0]['volumeMounts'] = [] - for (v_name, _, v_mount_path, _) in volumes: + for (v_name, _, v_mount_path) in volumes: containers[0]['volumeMounts'].append( {'name': v_name, 'mountPath': v_mount_path}) @@ -457,10 +454,9 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, configm if volumes: pod_data['spec']['volumes'] = [] - for (v_name, _, _, persistent) in volumes: - if persistent: - pod_data['spec']['volumes'].append( - {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) + for (v_name, _, _) in volumes: + pod_data['spec']['volumes'].append( + {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) if configmaps: pod_data['spec']['volumes'] = pod_data['spec'].get('volumes', []) @@ -516,7 +512,7 @@ def launch(self, inf, radl, requested_radl, num_vm, auth_data): inf.add_vm(vm) pod_name = re.sub('[!"#$%&\'()*+,/:;<=>?@[\\]^`{|}~_]', '-', system.name) - volumes = self._create_volumes(namespace, system, pod_name, auth_data, True) + volumes = self._create_volumes(namespace, system, pod_name, auth_data) configmaps = self._create_config_maps(namespace, system, pod_name, auth_data) From d0000f8c726b362141072f334b235bd97c340081 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 13:44:38 +0100 Subject: [PATCH 08/10] Improve code --- IM/connectors/Kubernetes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/IM/connectors/Kubernetes.py b/IM/connectors/Kubernetes.py index 14eb0650..0ee1089d 100644 --- a/IM/connectors/Kubernetes.py +++ b/IM/connectors/Kubernetes.py @@ -437,11 +437,18 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, configm if system.getValue("docker.privileged") == 'yes': containers[0]['securityContext'] = {'privileged': True} + pod_data['spec'] = {'restartPolicy': 'OnFailure'} + if volumes: containers[0]['volumeMounts'] = [] for (v_name, _, v_mount_path) in volumes: containers[0]['volumeMounts'].append( {'name': v_name, 'mountPath': v_mount_path}) + + pod_data['spec']['volumes'] = [] + for (v_name, _, _) in volumes: + pod_data['spec']['volumes'].append( + {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) if configmaps: containers[0]['volumeMounts'] = containers[0].get('volumeMounts', []) @@ -450,21 +457,14 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, configm {'name': cm_name, 'mountPath': cm_mount_path, "readOnly": True, 'subPath': os.path.basename(cm_mount_path)}) - pod_data['spec'] = {'containers': containers, 'restartPolicy': 'OnFailure'} - - if volumes: - pod_data['spec']['volumes'] = [] - for (v_name, _, _) in volumes: - pod_data['spec']['volumes'].append( - {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) - - if configmaps: pod_data['spec']['volumes'] = pod_data['spec'].get('volumes', []) for (cm_name, _) in configmaps: pod_data['spec']['volumes'].append( {'name': cm_name, 'configMap': {'name': cm_name}}) + pod_data['spec']['containers'] = containers + return pod_data @staticmethod From 75b388e43df9b0b91e4b83038b32357ee508193d Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 13:46:26 +0100 Subject: [PATCH 09/10] Improve code --- IM/connectors/Kubernetes.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/IM/connectors/Kubernetes.py b/IM/connectors/Kubernetes.py index 0ee1089d..eed219e9 100644 --- a/IM/connectors/Kubernetes.py +++ b/IM/connectors/Kubernetes.py @@ -441,24 +441,22 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, configm if volumes: containers[0]['volumeMounts'] = [] + pod_data['spec']['volumes'] = [] + for (v_name, _, v_mount_path) in volumes: containers[0]['volumeMounts'].append( {'name': v_name, 'mountPath': v_mount_path}) - - pod_data['spec']['volumes'] = [] - for (v_name, _, _) in volumes: pod_data['spec']['volumes'].append( {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) if configmaps: containers[0]['volumeMounts'] = containers[0].get('volumeMounts', []) + pod_data['spec']['volumes'] = pod_data['spec'].get('volumes', []) + for (cm_name, cm_mount_path) in configmaps: containers[0]['volumeMounts'].append( {'name': cm_name, 'mountPath': cm_mount_path, "readOnly": True, 'subPath': os.path.basename(cm_mount_path)}) - - pod_data['spec']['volumes'] = pod_data['spec'].get('volumes', []) - for (cm_name, _) in configmaps: pod_data['spec']['volumes'].append( {'name': cm_name, 'configMap': {'name': cm_name}}) From bf704b0e11681008f4a3eb6030b659710fb78148 Mon Sep 17 00:00:00 2001 From: Miguel Caballer Date: Fri, 23 Feb 2024 13:58:30 +0100 Subject: [PATCH 10/10] Fix test --- test/files/tosca_add.yml | 2 +- test/files/tosca_create.yml | 2 +- test/files/tosca_remove.yml | 2 +- test/files/tosca_remove_no_list.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/files/tosca_add.yml b/test/files/tosca_add.yml index 5e7231ea..51fb80ca 100644 --- a/test/files/tosca_add.yml +++ b/test/files/tosca_add.yml @@ -77,7 +77,7 @@ topology_template: interfaces: Standard: configure: - implementation: mysql/mysql_db_import.yml + implementation: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/mysql/mysql_db_import.yml inputs: db_name: { get_property: [ SELF, name ] } db_data: { get_artifact: [ SELF, db_content ] } diff --git a/test/files/tosca_create.yml b/test/files/tosca_create.yml index 1c72aee4..ce403f35 100644 --- a/test/files/tosca_create.yml +++ b/test/files/tosca_create.yml @@ -77,7 +77,7 @@ topology_template: interfaces: Standard: configure: - implementation: mysql/mysql_db_import.yml + implementation: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/mysql/mysql_db_import.yml inputs: db_name: { get_property: [ SELF, name ] } db_data: { get_artifact: [ SELF, db_content ] } diff --git a/test/files/tosca_remove.yml b/test/files/tosca_remove.yml index ce933966..ed5cd849 100644 --- a/test/files/tosca_remove.yml +++ b/test/files/tosca_remove.yml @@ -78,7 +78,7 @@ topology_template: interfaces: Standard: configure: - implementation: mysql/mysql_db_import.yml + implementation: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/mysql/mysql_db_import.yml inputs: db_name: { get_property: [ SELF, name ] } db_data: { get_artifact: [ SELF, db_content ] } diff --git a/test/files/tosca_remove_no_list.yml b/test/files/tosca_remove_no_list.yml index a00ac80b..16106b7d 100644 --- a/test/files/tosca_remove_no_list.yml +++ b/test/files/tosca_remove_no_list.yml @@ -78,7 +78,7 @@ topology_template: interfaces: Standard: configure: - implementation: mysql/mysql_db_import.yml + implementation: https://raw.githubusercontent.com/indigo-dc/tosca-types/master/artifacts/mysql/mysql_db_import.yml inputs: db_name: { get_property: [ SELF, name ] } db_data: { get_artifact: [ SELF, db_content ] }