From 502e83e335cfe31cba0f36b4debd69f333ec2197 Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 11 Oct 2023 13:21:42 +0400 Subject: [PATCH 1/7] feat: Added deleting a resource by its ID --- .../azure/resoto_plugin_azure/azure_client.py | 19 ++++++++++++++-- .../resoto_plugin_azure/resource/base.py | 22 ++++++++++++++----- plugins/azure/test/conftest.py | 3 +++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/plugins/azure/resoto_plugin_azure/azure_client.py b/plugins/azure/resoto_plugin_azure/azure_client.py index 0c50f8f14b..a9b85e6a78 100644 --- a/plugins/azure/resoto_plugin_azure/azure_client.py +++ b/plugins/azure/resoto_plugin_azure/azure_client.py @@ -41,6 +41,10 @@ def list(self, spec: AzureApiSpec, **kwargs: Any) -> List[Json]: def for_location(self, location: str) -> AzureClient: pass + @abstractmethod + def delete(self, resource_id: str) -> bool: + pass + @staticmethod def __create_management_client( credential: AzureCredentials, subscription_id: str, resource_group: Optional[str] = None @@ -66,8 +70,19 @@ def list(self, spec: AzureApiSpec, **kwargs: Any) -> List[Json]: else: raise e - def delete(self, resource_id: str) -> None: - self.client.resources.delete_by_id(resource_id) + def delete(self, resource_id: str) -> bool: + try: + self._delete_by_id(resource_id, api_version="2021-04-01") + except HttpResponseError as e: + if e.error and e.error.code == "ResourceNotFoundError": + return False + else: + raise e + + return True + + def _delete_by_id(self, resource_id: str, api_version: str) -> None: + self.client.resources._delete_by_id_initial(resource_id=resource_id, api_version=api_version) # noinspection PyProtectedMember def _call(self, spec: AzureApiSpec, **kwargs: Any) -> List[Json]: diff --git a/plugins/azure/resoto_plugin_azure/resource/base.py b/plugins/azure/resoto_plugin_azure/resource/base.py index 8eb21a3bc1..3c2ca8813f 100644 --- a/plugins/azure/resoto_plugin_azure/resource/base.py +++ b/plugins/azure/resoto_plugin_azure/resource/base.py @@ -9,7 +9,7 @@ from azure.core.utils import CaseInsensitiveDict from resoto_plugin_azure.azure_client import AzureApiSpec, AzureClient -from resoto_plugin_azure.config import AzureCredentials +from resoto_plugin_azure.config import AzureCredentials, AzureAccountConfig from resotolib.baseresources import BaseResource, Cloud, EdgeType, BaseAccount, BaseRegion, ModelReference from resotolib.core.actions import CoreFeedback from resotolib.graph import Graph, EdgeKey @@ -18,6 +18,14 @@ from resotolib.types import Json log = logging.getLogger("resoto.plugins.azure") + + +def get_client(builder: GraphBuilder) -> AzureClient: + subscription_id = builder.subscription.subscription_id + credential = AzureAccountConfig().credentials() + return AzureClient.create(credential=credential, subscription_id=subscription_id) + + T = TypeVar("T") @@ -28,10 +36,14 @@ class AzureResource(BaseResource): # Which API to call and what to expect in the result. api_spec: ClassVar[Optional[AzureApiSpec]] = None - def delete(self, graph: Any) -> bool: - # TODO: implement me. - # get_client().delete(self.id) - return False + def delete(self, graph: GraphBuilder) -> bool: + """ + Deletes a resource by ID. + + Returns: + bool: True if the resource was successfully deleted; False otherwise. + """ + return get_client(graph).delete(self.id) def pre_process(self, graph_builder: GraphBuilder, source: Json) -> None: """ diff --git a/plugins/azure/test/conftest.py b/plugins/azure/test/conftest.py index bc97daa8ad..4841ad2545 100644 --- a/plugins/azure/test/conftest.py +++ b/plugins/azure/test/conftest.py @@ -36,6 +36,9 @@ def create(*args: Any, **kwargs: Any) -> StaticFileAzureClient: def for_location(self, location: str) -> AzureClient: return self + def delete(self, resource_id: str) -> bool: + return False + @fixture def config() -> AzureConfig: From b25360a8d383d592ee953e5c9007e65b9d11f0cc Mon Sep 17 00:00:00 2001 From: Kirill Date: Wed, 11 Oct 2023 20:08:12 +0400 Subject: [PATCH 2/7] fix: Repaired resource deleting --- plugins/azure/resoto_plugin_azure/resource/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/azure/resoto_plugin_azure/resource/base.py b/plugins/azure/resoto_plugin_azure/resource/base.py index 3c2ca8813f..55f004c55c 100644 --- a/plugins/azure/resoto_plugin_azure/resource/base.py +++ b/plugins/azure/resoto_plugin_azure/resource/base.py @@ -20,8 +20,7 @@ log = logging.getLogger("resoto.plugins.azure") -def get_client(builder: GraphBuilder) -> AzureClient: - subscription_id = builder.subscription.subscription_id +def get_client(subscription_id: str) -> AzureClient: credential = AzureAccountConfig().credentials() return AzureClient.create(credential=credential, subscription_id=subscription_id) @@ -36,14 +35,15 @@ class AzureResource(BaseResource): # Which API to call and what to expect in the result. api_spec: ClassVar[Optional[AzureApiSpec]] = None - def delete(self, graph: GraphBuilder) -> bool: + def delete(self, graph: Graph) -> bool: """ Deletes a resource by ID. Returns: bool: True if the resource was successfully deleted; False otherwise. """ - return get_client(graph).delete(self.id) + subscription_id = self.id.split("/")[2] + return get_client(subscription_id).delete(self.id) def pre_process(self, graph_builder: GraphBuilder, source: Json) -> None: """ From 2750d4865b4c70a3606668dd1db1122dbec19be3 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 12 Oct 2023 09:21:26 +0400 Subject: [PATCH 3/7] chore: Optimize code for improved performance and readability --- plugins/azure/resoto_plugin_azure/azure_client.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/azure/resoto_plugin_azure/azure_client.py b/plugins/azure/resoto_plugin_azure/azure_client.py index a9b85e6a78..e7f330ed39 100644 --- a/plugins/azure/resoto_plugin_azure/azure_client.py +++ b/plugins/azure/resoto_plugin_azure/azure_client.py @@ -72,18 +72,15 @@ def list(self, spec: AzureApiSpec, **kwargs: Any) -> List[Json]: def delete(self, resource_id: str) -> bool: try: - self._delete_by_id(resource_id, api_version="2021-04-01") + self.client.resources._delete_by_id_initial(resource_id=resource_id, api_version="2021-04-01") except HttpResponseError as e: if e.error and e.error.code == "ResourceNotFoundError": - return False + return False # Resource not found to delete else: raise e return True - def _delete_by_id(self, resource_id: str, api_version: str) -> None: - self.client.resources._delete_by_id_initial(resource_id=resource_id, api_version=api_version) - # noinspection PyProtectedMember def _call(self, spec: AzureApiSpec, **kwargs: Any) -> List[Json]: _SERIALIZER = Serializer() From 16cca0c7acecbfa42503421ced60809ee3541142 Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 12 Oct 2023 13:57:03 +0400 Subject: [PATCH 4/7] fix: Fix the getting of credentials from config in get_client method --- plugins/azure/resoto_plugin_azure/resource/base.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/azure/resoto_plugin_azure/resource/base.py b/plugins/azure/resoto_plugin_azure/resource/base.py index 55f004c55c..b9c68e1a1d 100644 --- a/plugins/azure/resoto_plugin_azure/resource/base.py +++ b/plugins/azure/resoto_plugin_azure/resource/base.py @@ -3,7 +3,7 @@ import logging from concurrent.futures import Future from threading import Lock -from typing import Any, ClassVar, Dict, Optional, TypeVar, List, Type, Callable +from typing import Any, ClassVar, Dict, Optional, TypeVar, List, Type, Callable, cast from attr import define, field from azure.core.utils import CaseInsensitiveDict @@ -16,13 +16,15 @@ from resotolib.json_bender import Bender, bend, S, ForallBend, Bend from resotolib.threading import ExecutorQueue from resotolib.types import Json +from resotolib.config import current_config log = logging.getLogger("resoto.plugins.azure") def get_client(subscription_id: str) -> AzureClient: - credential = AzureAccountConfig().credentials() - return AzureClient.create(credential=credential, subscription_id=subscription_id) + config = current_config() + azure_config = cast(AzureAccountConfig, config.azure) + return AzureClient.create(credential=azure_config.credentials(), subscription_id=subscription_id) T = TypeVar("T") @@ -42,6 +44,8 @@ def delete(self, graph: Graph) -> bool: Returns: bool: True if the resource was successfully deleted; False otherwise. """ + # Extracts {subscriptionId} value from a resource_id + # e.g /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/... subscription_id = self.id.split("/")[2] return get_client(subscription_id).delete(self.id) From 4dfa355dfdf1a631d7569d9fd6c874326d0552ad Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 12 Oct 2023 13:59:47 +0400 Subject: [PATCH 5/7] fix: A correct call of Azure API method --- plugins/azure/resoto_plugin_azure/azure_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/azure/resoto_plugin_azure/azure_client.py b/plugins/azure/resoto_plugin_azure/azure_client.py index e7f330ed39..2be9b300e5 100644 --- a/plugins/azure/resoto_plugin_azure/azure_client.py +++ b/plugins/azure/resoto_plugin_azure/azure_client.py @@ -72,7 +72,7 @@ def list(self, spec: AzureApiSpec, **kwargs: Any) -> List[Json]: def delete(self, resource_id: str) -> bool: try: - self.client.resources._delete_by_id_initial(resource_id=resource_id, api_version="2021-04-01") + self.client.resources.begin_delete_by_id(resource_id=resource_id, api_version="2021-04-01") except HttpResponseError as e: if e.error and e.error.code == "ResourceNotFoundError": return False # Resource not found to delete From 726b988d3d56769f46049988d0aca988679cee0e Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 12 Oct 2023 16:09:02 +0400 Subject: [PATCH 6/7] Fixed getting credentials from config --- plugins/azure/resoto_plugin_azure/resource/base.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/azure/resoto_plugin_azure/resource/base.py b/plugins/azure/resoto_plugin_azure/resource/base.py index b9c68e1a1d..ce443ba95b 100644 --- a/plugins/azure/resoto_plugin_azure/resource/base.py +++ b/plugins/azure/resoto_plugin_azure/resource/base.py @@ -9,7 +9,7 @@ from azure.core.utils import CaseInsensitiveDict from resoto_plugin_azure.azure_client import AzureApiSpec, AzureClient -from resoto_plugin_azure.config import AzureCredentials, AzureAccountConfig +from resoto_plugin_azure.config import AzureConfig, AzureCredentials from resotolib.baseresources import BaseResource, Cloud, EdgeType, BaseAccount, BaseRegion, ModelReference from resotolib.core.actions import CoreFeedback from resotolib.graph import Graph, EdgeKey @@ -23,8 +23,11 @@ def get_client(subscription_id: str) -> AzureClient: config = current_config() - azure_config = cast(AzureAccountConfig, config.azure) - return AzureClient.create(credential=azure_config.credentials(), subscription_id=subscription_id) + azure_config = cast(AzureConfig, config.azure) + # Taking credentials from the config if access through the environment cannot be provided + if azure_config.accounts and (account := azure_config.accounts[subscription_id]): + credential = account.credentials() + return AzureClient.create(credential=credential, subscription_id=subscription_id) T = TypeVar("T") From 44fcfe139682cacc341525503eca137d48eacc4c Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 12 Oct 2023 17:28:32 +0400 Subject: [PATCH 7/7] feat: Added handler of config if is it empty --- plugins/azure/resoto_plugin_azure/resource/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/azure/resoto_plugin_azure/resource/base.py b/plugins/azure/resoto_plugin_azure/resource/base.py index ce443ba95b..68ceb1154a 100644 --- a/plugins/azure/resoto_plugin_azure/resource/base.py +++ b/plugins/azure/resoto_plugin_azure/resource/base.py @@ -7,6 +7,7 @@ from attr import define, field from azure.core.utils import CaseInsensitiveDict +from azure.identity import DefaultAzureCredential from resoto_plugin_azure.azure_client import AzureApiSpec, AzureClient from resoto_plugin_azure.config import AzureConfig, AzureCredentials @@ -25,8 +26,10 @@ def get_client(subscription_id: str) -> AzureClient: config = current_config() azure_config = cast(AzureConfig, config.azure) # Taking credentials from the config if access through the environment cannot be provided - if azure_config.accounts and (account := azure_config.accounts[subscription_id]): + if azure_config.accounts and (account := azure_config.accounts.get(subscription_id)): credential = account.credentials() + else: + credential = DefaultAzureCredential() return AzureClient.create(credential=credential, subscription_id=subscription_id)