From 4ea05996700d90d9fc2d4bf1d60603b662c98822 Mon Sep 17 00:00:00 2001 From: Quan Pham Date: Sun, 20 Apr 2025 11:30:18 -0400 Subject: [PATCH] Allow adding default labels to Openshift allocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All Openshift projects created through Coldfront will now have an additional label: `'nerc.mghpcc.org/allow-unencrypted-routes': "true"` `validate_allocations` has also been changed to ensure all current Openshift projects contain a few default labels: ``` PROJECT_DEFAULT_LABELS = { 'opendatahub.io/dashboard': "true", 'modelmesh-enabled': "true", 'nerc.mghpcc.org/allow-unencrypted-routes': "true" } ``` Implementing this required code in the Openshift allocator to interact with the Namespace API. For reasons not entirely clear in the documentation, it is not possible change a Project’s labels directly. This is only possible through the Namespace API. --- .../commands/validate_allocations.py | 17 ++++++++++ src/coldfront_plugin_cloud/openshift.py | 26 ++++++++++++--- .../functional/openshift/test_allocation.py | 32 +++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/coldfront_plugin_cloud/management/commands/validate_allocations.py b/src/coldfront_plugin_cloud/management/commands/validate_allocations.py index 469fde6e..2020193a 100644 --- a/src/coldfront_plugin_cloud/management/commands/validate_allocations.py +++ b/src/coldfront_plugin_cloud/management/commands/validate_allocations.py @@ -52,6 +52,22 @@ def sync_users(project_id, allocation, allocator, apply): return failed_validation + @staticmethod + def sync_openshift_project_labels(project_id, allocator, apply): + cloud_namespace_obj = allocator._openshift_get_namespace(project_id) + cloud_namespace_obj_labels = cloud_namespace_obj["metadata"]["labels"] + if missing_or_incorrect_labels := [ + label_items[0] for label_items in openshift.PROJECT_DEFAULT_LABELS.items() if + label_items not in cloud_namespace_obj_labels.items() + ]: + logger.warning( + f"Openshift project {project_id} is missing default labels: {', '.join(missing_or_incorrect_labels)}" + ) + cloud_namespace_obj_labels.update(openshift.PROJECT_DEFAULT_LABELS) + if apply: + allocator.patch_project(project_id, cloud_namespace_obj) + logger.warning(f"Labels updated for Openshift project {project_id}: {', '.join(missing_or_incorrect_labels)}") + def check_institution_specific_code(self, allocation, apply): attr = attributes.ALLOCATION_INSTITUTION_SPECIFIC_CODE isc = allocation.get_attribute(attr) @@ -183,6 +199,7 @@ def handle(self, *args, **options): quota = allocator.get_quota(project_id) failed_validation = Command.sync_users(project_id, allocation, allocator, options["apply"]) + Command.sync_openshift_project_labels(project_id, allocator, options["apply"]) for attr in tasks.get_expected_attributes(allocator): key_with_lambda = allocator.QUOTA_KEY_MAPPING.get(attr, None) diff --git a/src/coldfront_plugin_cloud/openshift.py b/src/coldfront_plugin_cloud/openshift.py index abe5e2dd..67109275 100644 --- a/src/coldfront_plugin_cloud/openshift.py +++ b/src/coldfront_plugin_cloud/openshift.py @@ -27,6 +27,14 @@ "uid", ] + +PROJECT_DEFAULT_LABELS = { + 'opendatahub.io/dashboard': "true", + 'modelmesh-enabled': "true", + 'nerc.mghpcc.org/allow-unencrypted-routes': "true" +} + + def clean_openshift_metadata(obj): if "metadata" in obj: for attr in IGNORED_ATTRIBUTES: @@ -146,6 +154,9 @@ def create_project(self, suggested_project_name): self._create_project(project_name, project_id) return self.Project(project_name, project_id) + def patch_project(self, project_id, new_project_spec): + self._openshift_patch_namespace(project_id, new_project_spec) + def delete_moc_quotas(self, project_id): """deletes all resourcequotas from an openshift project""" resourcequotas = self._openshift_get_resourcequotas(project_id) @@ -238,14 +249,10 @@ def _create_project(self, project_name, project_id): headers = {"Content-type": "application/json"} annotations = {"cf_project_id": str(self.allocation.project_id), "cf_pi": self.allocation.project.pi.username} - labels = { - 'opendatahub.io/dashboard': "true", - 'modelmesh-enabled': "true", - } payload = {"displayName": project_name, "annotations": annotations, - "labels": labels} + "labels": PROJECT_DEFAULT_LABELS} r = self.session.put(url, data=json.dumps(payload), headers=headers) self.check_response(r) @@ -323,6 +330,15 @@ def _openshift_useridentitymapping_exists(self, user_name, id_user): def _openshift_get_project(self, project_name): api = self.get_resource_api(API_PROJECT, "Project") return clean_openshift_metadata(api.get(name=project_name).to_dict()) + + def _openshift_get_namespace(self, namespace_name): + api = self.get_resource_api(API_CORE, "Namespace") + return clean_openshift_metadata(api.get(name=namespace_name).to_dict()) + + def _openshift_patch_namespace(self, project_name, new_project_spec): + # During testing, apparently we can't patch Projects, but we can do so with Namespaces + api = self.get_resource_api(API_CORE, "Namespace") + res = api.patch(name=project_name, body=new_project_spec) def _openshift_get_resourcequotas(self, project_id): """Returns a list of resourcequota objects in namespace with name `project_id`""" diff --git a/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py b/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py index 8e4c10bc..d28cbcf5 100644 --- a/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py +++ b/src/coldfront_plugin_cloud/tests/functional/openshift/test_allocation.py @@ -214,3 +214,35 @@ def test_reactivate_allocation(self): }) allocator._get_role(user.username, project_id) + + def test_project_default_labels(self): + user = self.new_user() + project = self.new_project(pi=user) + allocation = self.new_allocation(project, self.resource, 1) + allocator = openshift.OpenShiftResourceAllocator(self.resource, + allocation) + + tasks.activate_allocation(allocation.pk) + allocation.refresh_from_db() + + project_id = allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID) + + # Check project labels + namespace_dict_labels = allocator._openshift_get_namespace(project_id)["metadata"]["labels"] + self.assertTrue(namespace_dict_labels.items() > openshift.PROJECT_DEFAULT_LABELS.items()) + + # What if we have a new custom label, or changed value? + openshift.PROJECT_DEFAULT_LABELS["test"] = "test" + call_command('validate_allocations', apply=True) + + namespace_dict_labels = allocator._openshift_get_namespace(project_id)["metadata"]["labels"] + self.assertTrue(namespace_dict_labels.items() > openshift.PROJECT_DEFAULT_LABELS.items()) + + # What if a deafult label is removed (or cloud label + # already has other unrelated labels)? Cloud label should still remain + del openshift.PROJECT_DEFAULT_LABELS["test"] + call_command('validate_allocations', apply=True) + namespace_dict_labels = allocator._openshift_get_namespace(project_id)["metadata"]["labels"] + self.assertFalse(openshift.PROJECT_DEFAULT_LABELS.items() > {"test": "test"}.items()) + self.assertTrue(namespace_dict_labels.items() > {"test": "test"}.items()) + self.assertTrue(namespace_dict_labels.items() > openshift.PROJECT_DEFAULT_LABELS.items())