Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Nuclio] Delete remote function config map #5530

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion server/api/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1353,10 +1353,17 @@ async def delete_function(
project: str,
function: str,
_semaphore: asyncio.Semaphore,
k8s_helper: server.api.utils.singletons.k8s.K8sHelper,
) -> tuple[str, str]:
async with _semaphore:
try:
await nuclio_client.delete_function(name=function, project_name=project)

config_map = k8s_helper.get_configmap(
function, mlrun.common.constants.MLRUN_MODEL_CONF
)
if config_map:
k8s_helper.delete_configmap(config_map.metadata.name)
return None
except Exception as exc:
# return tuple with failure info
Expand All @@ -1370,8 +1377,9 @@ async def delete_function(
failed_requests = []

async with server.api.utils.clients.async_nuclio.Client(auth_info) as client:
k8s_helper = server.api.utils.singletons.k8s.get_k8s_helper()
tasks = [
delete_function(client, project_name, function_name, semaphore)
delete_function(client, project_name, function_name, semaphore, k8s_helper)
for function_name in function_names
]

Expand Down
43 changes: 25 additions & 18 deletions server/api/utils/singletons/k8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,28 +552,20 @@ def ensure_configmap(
labels: dict = None,
):
namespace = self.resolve_namespace(namespace)

have_confmap = False
label_name = "resource_name"
full_name = f"{resource}-{name}"
name = (
full_name
if len(full_name) <= 63
else full_name[:59] + self._generate_rand_string(4)
)

have_confmap = False
configmaps_with_label = self.v1api.list_namespaced_config_map(
namespace=namespace, label_selector=f"{label_name}={full_name}"
)

if len(configmaps_with_label.items) > 1:
raise mlrun.errors.MLRunInternalServerError(
f"Received more than one config map for label: {full_name}"
)

if len(configmaps_with_label.items) == 1:
name = configmaps_with_label.items[0].metadata.name
configmap_with_label = self.get_configmap(name, resource, namespace)
if configmap_with_label:
name = configmap_with_label.metadata.name
have_confmap = True
else:
name = (
full_name
if len(full_name) <= 63
else full_name[:59] + self._generate_rand_string(4)
)

if labels is None:
labels = {label_name: full_name}
Expand Down Expand Up @@ -610,6 +602,21 @@ def ensure_configmap(
raise exc
return name

@raise_for_status_code
def get_configmap(self, name: str, resource: str, namespace: str = ""):
namespace = self.resolve_namespace(namespace)
label_name = "resource_name"
full_name = f"{resource}-{name}"
configmaps_with_label = self.v1api.list_namespaced_config_map(
namespace=namespace, label_selector=f"{label_name}={full_name}"
)
if len(configmaps_with_label.items) > 1:
raise mlrun.errors.MLRunInternalServerError(
f"Received more than one config map for label: {full_name}"
)

return configmaps_with_label.items[0] if configmaps_with_label.items else None

@raise_for_status_code
def delete_configmap(self, name: str, namespace: str = "", raise_on_error=True):
namespace = self.resolve_namespace(namespace)
Expand Down
32 changes: 32 additions & 0 deletions tests/api/api/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import json
import unittest.mock
from http import HTTPStatus
from unittest.mock import AsyncMock, MagicMock, patch

import kubernetes.client
import pytest
from deepdiff import DeepDiff
from fastapi.testclient import TestClient
from kubernetes.client.models import V1ConfigMap, V1ObjectMeta
from sqlalchemy.orm import Session

import mlrun
Expand All @@ -35,6 +37,7 @@
from mlrun.common.schemas import SecurityContextEnrichmentModes
from mlrun.utils import logger
from server.api.api.utils import (
_delete_nuclio_functions_in_batches,
_generate_function_and_task_from_submit_run_body,
_mask_v3io_access_key_env_var,
_mask_v3io_volume_credentials,
Expand Down Expand Up @@ -1658,3 +1661,32 @@ def _assert_env_var_from_secret(
)
assert env_var_value.secret_key_ref.name == secret_name
assert env_var_value.secret_key_ref.key == secret_key


@pytest.mark.asyncio
async def test_delete_function_calls_k8s_helper_methods():
function_names = ["function1"]
async_client_mock = AsyncMock()

k8s_helper_mock = MagicMock()
configmap = V1ConfigMap()
configmap.metadata = V1ObjectMeta(name="config-map-1")
k8s_helper_mock.get_configmap.return_value = configmap

with (
patch(
"server.api.utils.clients.async_nuclio.Client",
return_value=async_client_mock,
),
patch(
"server.api.utils.singletons.k8s.get_k8s_helper",
return_value=k8s_helper_mock,
),
):
failed_requests = await _delete_nuclio_functions_in_batches(
{}, "my-project", function_names
)

assert len(failed_requests) == 0
k8s_helper_mock.get_configmap.assert_called_with("function1", "model-conf")
k8s_helper_mock.delete_configmap.assert_called_with("config-map-1")
Loading