diff --git a/IM/connectors/Kubernetes.py b/IM/connectors/Kubernetes.py index 542db4c6..eed219e9 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,22 +156,84 @@ def _create_volume_claim(self, claim_data, auth_data): self.log_exception("Error connecting with Kubernetes API server") return False - def _create_volumes(self, namespace, system, pod_name, auth_data, persistent=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) + ".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 = {'apiVersion': 'v1', 'kind': 'PersistentVolumeClaim'} - claim_data['metadata'] = {'name': name, 'namespace': namespace} + 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): + res = [] + cont = 1 + 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) + + claim_data = self._gen_basic_k8s_elem(namespace, name, 'PersistentVolumeClaim') claim_data['spec'] = {'accessModes': ['ReadWriteOnce'], 'resources': { 'requests': {'storage': disk_size}}} @@ -181,11 +244,9 @@ def _create_volumes(self, namespace, system, pod_name, auth_data, persistent=Fal 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)) + res.append((name, disk_size, disk_mount_path)) else: self.log_error("Error creating PersistentVolumeClaim:" + name) - else: - res.append((name, disk_size, disk_mount_path, persistent)) cont += 1 @@ -218,12 +279,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 +325,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 +377,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 = [] @@ -338,7 +399,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")) @@ -353,13 +414,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(): @@ -382,20 +437,31 @@ def _generate_pod_data(self, namespace, name, outports, system, volumes, tags): 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: + 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'].append( + {'name': v_name, 'persistentVolumeClaim': {'claimName': v_name}}) - pod_data['spec'] = {'containers': containers, 'restartPolicy': 'OnFailure'} + if configmaps: + containers[0]['volumeMounts'] = containers[0].get('volumeMounts', []) + pod_data['spec']['volumes'] = pod_data['spec'].get('volumes', []) - 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 (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'].append( + {'name': cm_name, + 'configMap': {'name': cm_name}}) + + pod_data['spec']['containers'] = containers return pod_data @@ -444,7 +510,9 @@ 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) tags = self.get_instance_tags(system, auth_data, inf) @@ -456,7 +524,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") @@ -469,15 +537,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() @@ -485,7 +558,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: @@ -575,6 +647,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..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,21 +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 = [] - 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") == "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) - 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): - raise Exception("Only Docker images are supported for K8s container.") + + cont = self._gen_k8s_configmaps(res, cms) + repo = artifact.get("repository", None) if repo: repo_url = self._get_repository_url(repo) @@ -2268,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_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_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/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 ] } 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') diff --git a/test/unit/connectors/Kubernetes.py b/test/unit/connectors/Kubernetes.py index 6d81b25e..b1aade94 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() @@ -159,10 +168,15 @@ 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", - "metadata": {"name": "test-1", "namespace": "somenamespace"}, + "metadata": {"name": "test-1", + "namespace": "somenamespace", + 'labels': {'name': 'test-1'}}, "spec": { "accessModes": ["ReadWriteOnce"], "resources": {"requests": {"storage": 10737418240}}, @@ -172,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", @@ -192,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, 'subPath': 'config'}], } ], "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"}}, ], }, } - 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", @@ -227,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 = { @@ -270,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()) @@ -362,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.")